Rate Limiting¶
Ellar-Throttler
package offers a robust rate-limiting module designed specifically for Ellar, providing efficient throttling capabilities for web applications.
Installation¶
To install the Ellar Throttler package, use pip within your virtual environment:
$(venv) pip install ellar-throttler
Usage¶
ThrottlerModule¶
The ThrottlerModule
serves as the primary interface for configuring throttling mechanisms across the entire Ellar application.
ThrottlerModule Parameters¶
Parameter | Description |
---|---|
throttlers | A list of IThrottleModel instances defining various throttling mechanisms. |
storage | An IThrottlerStorage service, instance, or class responsible for tracking throttling. |
error_message | A customizable string to replace the default throttler error message. |
ignore_user_agents | An array of user-agent strings to exempt from throttling. |
skip_if | A global function that evaluates ExecutionContext and returns a boolean to bypass throttler logic. |
ThrottlerModule Setup¶
Similar to other Ellar modules, you can set up ThrottlerModule
directly using the setup
function or configure it via register_setup
.
from ellar.common import Module
from ellar_throttler import ThrottlerModule, AnonymousThrottler, UserThrottler
@Module(modules=[
ThrottlerModule.setup(
throttlers=[
AnonymousThrottler(limit=100, ttl=(60*5), name='anon'), # Allow 100 requests per 5 minutes for anonymous users
UserThrottler(limit=2000, ttl=(60*60*24), name='user'), # Allow 2000 requests per day for authenticated users
]
)
])
class ApplicationModule:
pass
In the above setup, we've specified a limit of 100 requests per 5 minutes for anonymous users and 2000 requests per day for authenticated users.
Alternatively, configuration-based setup is demonstrated below:
...
from ellar_throttler import AnonymousThrottler, UserThrottler
class BaseConfig:
ELLAR_THROTTLER_CONFIG: {
'throttlers': [
AnonymousThrottler(limit=100, ttl=(60*5), name='anon'), # Allow 100 requests per 5 minutes for anonymous users
UserThrottler(limit=2000, ttl=(60*60*24), name='user'), # Allow 2000 requests per day for authenticated users
]
}
Then, in ApplicationModule
:
from ellar.common import Module
from ellar_throttler import ThrottlerModule
@Module(modules=[
ThrottlerModule.register_setup()
])
class ApplicationModule:
pass
Throttle All Routes¶
To apply throttling to all incoming requests, utilize ThrottlerInterceptor
globally, as illustrated below:
from ellar.app import AppFactory
from ellar_throttler import ThrottlerInterceptor
from .module import AppModule
app = AppFactory.create_from_app_module(AppModule)
app.use_global_interceptors(ThrottlerInterceptor)
Decorators¶
This package introduces two decorators, Throttle
and SkipThrottle
, designed to provide additional metadata for the ThrottlerInterceptor
, guiding its throttling behavior or bypassing it for decorated controllers or route functions.
Using Throttle
Decorator¶
The Throttle
decorator applies the ThrottlerInterceptor
and allows for overriding configurations for any throttler model defined in the ThrottlerModule.throttlers
list.
from ellar_throttler import Throttle
from ellar.common import Controller, get
@Controller("/limit")
@Throttle(intercept=True)
class LimitController:
def __init__(self, app_service: AppService):
self.app_service = app_service
@get()
def get_throttled(self, use_auth: bool):
return self.app_service.success(use_auth)
In the above example, by setting intercept=True
within @Throttle, the ThrottlerInterceptor
is applied to all routes within the LimitController
. This feature is particularly useful when ThrottlerInterceptor
is not globally applied.
Additionally, the @Throttle decorator can be used on a route level:
@get()
@Throttle(intercept=True)
def get_throttled(self, use_auth: bool):
return self.app_service.success(use_auth)
Another application of the @Throttle decorator is to override ttl
and limit
for a specific configured throttler model:
@Controller("/limit")
@Throttle(intercept=True, anon={'ttl': 100, 'limit': 30})
class LimitController:
def __init__(self, app_service: AppService):
self.app_service = app_service
@get()
def get_throttled(self, use_auth: bool):
return self.app_service.success(use_auth)
In this scenario, different ttl
and limit
values are applied to the LimitController when executing against a throttler model named anon
.
Using SkipThrottle
Decorator¶
The SkipThrottle
decorator marks a decorated class or route function with metadata used by ThrottlerInterceptor
to entirely bypass throttling or selectively skip specific throttler models.
For instance, if using ThrottlerInterceptor
as a global interceptor and wishing to bypass throttling for LimitController
:
from ellar_throttler import SkipThrottle
from ellar.common import Controller, get
@Controller("/limit")
@SkipThrottle()
class LimitController:
def __init__(self, app_service: AppService):
self.app_service = app_service
@get()
def get_throttled(self, use_auth: bool):
return self.app_service.success(use_auth)
This setup will bypass all throttling models for LimitController
. If only the user
throttler model needs to be skipped:
from ellar_throttler import SkipThrottle
from ellar.common import Controller, get
@Controller("/limit")
@SkipThrottle(user=True)
class LimitController:
def __init__(self, app_service: AppService):
self.app_service = app_service
@get()
def get_throttled(self, use_auth: bool):
return self.app_service.success(use_auth)
This configuration skips all throttling models except for the one named user
.
IThrottlerModel¶
The IThrottlerModel serves as an interface for defining attributes and properties specific to a throttler. The package provides several ThrottlerModel
implementations to facilitate usage.
AnonymousThrottler¶
The AnonymousThrottler
model is designed for throttling unauthenticated users, utilizing their client.host
address as a key for throttling purposes. It exempts authenticated requests and is ideal for limiting requests from unknown sources.
from ellar_throttler import AnonymousThrottler
class BaseConfig:
ELLAR_THROTTLER_CONFIG: {
'throttlers': [
# Configuring burst and sustained throttling for unauthenticated users
AnonymousThrottler(limit=100, ttl=(60*5), name='burst'),
AnonymousThrottler(limit=1000, ttl=(60*60*24), name='sustain'),
]
}
UserThrottler¶
The UserThrottler
model is tailored for throttling authenticated users, utilizing user id
or sub
to generate a key for throttling. Unauthenticated requests resort to using the client.host
address for generating a unique key.
from ellar_throttler import UserThrottler
class BaseConfig:
ELLAR_THROTTLER_CONFIG: {
'throttlers': [
# Configuring burst and sustained throttling for authenticated users
UserThrottler(limit=100, ttl=(60*5), name='burst'),
UserThrottler(limit=1000, ttl=(60*60*24), name='sustain'),
]
}
IThrottlerStorage¶
The IThrottlerStorage
interface defines methods for managing the details of request tracking within the throttler. This package provides two implementations of IThrottlerStorage
:
ThrottlerStorageService
:¶
This service offers in-memory storage for throttling, suitable for testing request throttling in development environments. When configuring ThrottlerModule
, if no storage parameter is provided, ThrottlerStorageService
is selected by default.
from ellar.common import Module
from ellar_throttler import ThrottlerModule, AnonymousThrottler, UserThrottler, ThrottlerStorageService
@Module(modules=[
ThrottlerModule.setup(
throttlers=[
AnonymousThrottler(limit=200, ttl=(60*24), name='anon'), # 200/day for anonymous requests
UserThrottler(limit=1000, ttl=(60*24), name='user'), # 1000/day for authenticated requests
],
# storage=ThrottlerStorageService() use as an instance OR
storage=ThrottlerStorageService
)
])
class ApplicationModule:
pass
CacheThrottlerStorageService
¶
This service utilizes the default caching setup in your Ellar application, providing more dynamic storage options. It requires setting up caching, where various storage backends are available through CacheModule
. In the example below, caching is set up using the Redis backend.
from ellar.common import Module
from ellar.cache import CacheModule
from ellar.cache.backends.redis import RedisCacheBackend
from ellar_throttler import ThrottlerModule, AnonymousThrottler, UserThrottler, CacheThrottlerStorageService
@Module(modules=[
ThrottlerModule.setup(
throttlers=[
AnonymousThrottler(limit=100, ttl=(60*5), name='anon'), # 200/5mins for anonymous requests
UserThrottler(limit=2000, ttl=(60*60*24), name='user'), # 2000/24hrs for authenticated requests
],
storage=CacheThrottlerStorageService
),
CacheModule.setup(default=RedisCacheBackend(servers=['redis://127.0.0.1:6379']))
])
class ApplicationModule:
pass
To modify the caching type used in CacheThrottlerStorageService
, you need to extend the class and set the cache_backend
to point to the desired caching service backend:
from ellar.di import injectable
from ellar.cache import ICacheService, CacheModule
from ellar_throttler import CacheThrottlerStorageService, ThrottlerModule
@injectable()
class MyNewCacheThrottlerStorageService(CacheThrottlerStorageService):
def __init__(self, cache_service: ICacheService) -> None:
super().__init__(cache_service)
self.cache_backend = 'my_backend'
# in root_module.py
@Module(modules=[
ThrottlerModule.setup(
throttlers=[
AnonymousThrottler(limit=100, ttl=(60*5), name='anon'), # 200/5mins for anonymous requests
UserThrottler(limit=2000, ttl=(60*60*24), name='user'), # 2000/24hrs for authenticated requests
],
storage=MyNewCacheThrottlerStorageService
),
CacheModule.setup(
default=RedisCacheBackend(servers=['redis://127.0.0.1:6379']),
my_backend=RedisCacheBackend(servers=['redis://127.0.0.1:6379'])
)
])
class ApplicationModule:
pass
Proxies¶
If you're working with multiple proxies, you may need to install ProxyHeadersMiddleware
.
Working with WebSockets¶
To utilize WebSockets, you can include WebsocketThrottler
in the list of throttlers for your application.
from ellar.common import Module
from ellar.cache import CacheModule
from ellar.cache.backends.redis import RedisCacheBackend
from ellar_throttler import ThrottlerModule, AnonymousThrottler, UserThrottler, CacheThrottlerStorageService, model
@Module(modules=[
ThrottlerModule.setup(
throttlers=[
# HTTP Throttling Models
AnonymousThrottler(limit=200, ttl=(60*24)), # 200/day for anonymous requests
UserThrottler(limit=1000, ttl=(60*24)), # 1000/day for authenticated requests
# Websocket Throttling Models
model.WebsocketThrottler('ws-burst', limit=200, ttl=(60*24)), # 200/day
model.WebsocketThrottler('ws-sustain', limit=2000, ttl=(60*24)), # 2000/day
],
storage=CacheThrottlerStorageService
),
CacheModule.setup(default=RedisCacheBackend(servers=['redis://127.0.0.1:6379']))
])
class ApplicationModule:
pass
The WebsocketThrottler
only runs within the websocket environment. Additionally, note that WebsocketThrottler
workers like UserThrottler
for an authenticated request and AnonymousThrottler
for anonymous request.
Inline Throttling Definition¶
It is possible to define throttling models at the point usage and they will override the globally registered models.
For example:
@get("/shorter-inline-throttling")
@Throttle(AnonymousThrottler(ttl=5, limit=3), UserThrottler(ttl=3, limit=3)) # overriding global throttling options
def get_shorter_inline_version(self, use_auth: bool):
return self.app_service.success(use_auth)
In get_shorter_inline_version
, AnonymousThrottler(ttl=5, limit=3)
and UserThrottler(ttl=3, limit=3)
throttling models were defined in it. This will be used as throttling models for get_shorter_inline_version
during runtime.