Welcome to Falcon-Limiter’s documentation!

Version: 1.0.1

Falcon-Limiter provides advanced rate limiting support to the Falcon web framework.

Rate limiting strategies are provided with the help of the popular Limits library.

The library aims to be compatible with CPython 3.6+ and PyPy 3.5+.

Quickstart

WSGI

Quick example - using fixed-window strategy and storing the hits against limits in the memory:

import falcon
from falcon_limiter import Limiter
from falcon_limiter.utils import get_remote_addr

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits="5 per minute,2 per second"
)

# use the default limit for all methods of this class
@limiter.limit()
class ThingsResource:
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

# add the limiter middleware to the Falcon app
app = falcon.API(middleware=limiter.middleware)

things = ThingsResource()
app.add_route('/things', things)

ASGI (Async)

Quick example - using fixed-window strategy and storing the hits against limits in the memory:

import falcon.asgi
from falcon_limiter import AsyncLimiter
from falcon_limiter.utils import get_remote_addr

limiter = AsyncLimiter(
    key_func=get_remote_addr,
    default_limits="5 per minute,2 per second"
)

# use the default limit for all methods of this class
@limiter.limit()
class ThingsResource:
    async def on_get(self, req, resp):
        resp.body = 'Hello world!'

# add the limiter middleware to the Falcon app
app = falcon.asgi.App(middleware=limiter.middleware)

things = ThingsResource()
app.add_route('/things', things)

See Async (experimental) for more about Async.

A more complicated example

When making calls against this app, above >5 calls per minute or >2 per seconds you will receive an HTTP 429 error response with message: “Reached allowed limit 5 hits per 1 minute!”

A second, more complicated example - using the moving-window strategy with a shared Redis backend and running the application behind a reverse proxy:

import falcon
from falcon_limiter import Limiter

# a custom key function
def get_access_route_addr(req, resp, resource, params) -> str:
    """ Get the requestor's IP by discounting 1 reverse proxy
    """
    return req.access_route[-2]

limiter = Limiter(
    key_func=get_access_route_addr,
    default_limits="5 per minute,2 per second",
    # only count HTTP 200 responses against the limit:
    default_deduct_when=lambda req, resp, resource, req_succeeded:
        resp.status == falcon.HTTP_200,
    config={
        'RATELIMIT_KEY_PREFIX': 'myapp',  # to allow multiple apps in the same Redis db
        'RATELIMIT_STORAGE_URL': f'redis://:{REDIS_PSW}@{REDIS_HOST}:{REDIS_PORT}',
        'RATELIMIT_STRATEGY': 'moving-window'
    }
)

class ThingsResource:
    # no rate limit on this method
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

    # a more strict rate limit applied to this method
    # with a custom key function serving up the user_id
    # from the request context as key
    @limiter.limit(limits="3 per minute,1 per second",
        key_func=lambda req, resp, resource, params: req.context.user_id)
    def on_post(self, req, resp):
        resp.body = 'Hello world!'

class SpecialResource:
    # dynamic_limits allowing the 'admin' user a higher limit than others
    @limiter.limit(dynamic_limits=lambda req, resp, resource, params:
        '999/minute,9999/second' if req.context.user == 'admin'
        else '5 per minute,2/second')
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

# add the limiter middleware to the Falcon app
app = falcon.API(middleware=limiter.middleware)

things = ThingsResource()
special = SpecialResource()
app.add_route('/things', things)
app.add_route('/special', special)

Installation

Install the extension with pip:

$ pip install Falcon-Limiter

Set Up

Rate limiting is managed through a Limiter instance:

import falcon
from falcon_limiter import Limiter
from falcon_limiter.utils import get_remote_addr

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits="5 per minute,2 per second"
)

The Limiter instance is a Falcon Middleware and it has a limit() method which can be used as a decorator to decorate a whole class or individual methods:

import falcon
from falcon_limiter import Limiter
from falcon_limiter.utils import get_remote_addr

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits="5 per minute,2 per second"
)

# use the default limit for all methods of this class
@limiter.limit()
class ThingsResource:
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

# use the default limit for all methods of this class
@limiter.limit()
class ThingsResource2:
    # this will use the default limit from the class
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

    # this will use a custom limit overwriting the one set at class level
    @limiter.limit(limits="3 per minute,1 per second")
    def on_post(self, req, resp):
        resp.body = 'Hello world!'

You can provide a config dictionary to the Limiter, see Configuring Falcon-Limiter:

import falcon
from falcon_limiter import Limiter
from falcon_limiter.utils import get_remote_addr

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits="5 per minute,2 per second",
    config={
        'RATELIMIT_KEY_PREFIX': 'myapp',  # to allow multiple apps in the same Redis db
        'RATELIMIT_STORAGE_URL': f'redis://:{REDIS_PSW}@{REDIS_HOST}:{REDIS_PORT}',
        'RATELIMIT_STRATEGY': 'moving-window'
    }
)

The limiter instance needs to be specified as a middleware when creating the app by calling falcon.API():

import falcon
from falcon_limiter import Limiter
from falcon_limiter.utils import get_remote_addr

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits="5 per minute,2 per second"
)

@limiter.limit()
class ThingsResource:
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

# add the limiter middleware to the Falcon app
app = falcon.API(middleware=limiter.middleware)

Rate limit string notation

Rate limits are specified as strings following the format:

[count] [per|/] [n (optional)] [second|minute|hour|day|month|year]

You can combine multiple rate limits by separating them with a delimiter of your choice.

Examples

  • 10 per hour
  • 10/hour
  • 10/hour;100/day;2000 per year
  • 100/day, 500/7days

Rate limiting strategies

Falcon-Limiter comes with three different rate limiting strategies built-in, provided by the Limits library.

Pick the one that works for your use-case by specifying it in your config as RATELIMIT_STRATEGY (one of fixed-window, fixed-window-elastic-expiry, or moving-window). The default configuration is fixed-window.

Fixed Window

This is the most memory efficient strategy to use as it maintains one counter per resource and rate limit. It does however have its drawbacks as it allows bursts within each window - thus allowing an ‘attacker’ to by-pass the limits. The effects of these bursts can be partially circumvented by enforcing multiple granularities of windows per resource.

For example, if you specify a 100/minute rate limit on a route, this strategy will allow 100 hits in the last second of one window and a 100 more in the first second of the next window. To ensure that such bursts are managed, you could add a second rate limit of 2/second on the same route.

Fixed Window with Elastic Expiry

This strategy works almost identically to the Fixed Window strategy with the exception that each hit results in the extension of the window. This strategy works well for creating large penalties for breaching a rate limit.

For example, if you specify a 100/minute rate limit on a route and it is being attacked at the rate of 5 hits per second for 2 minutes - the attacker will be locked out of the resource for an extra 60 seconds after the last hit. This strategy helps circumvent bursts.

Moving Window

Warning

The moving window strategy is only implemented for the redis and in-memory storage backends. The strategy requires using a list with fast random access which is not very convenient to implement with a memcached storage.

This strategy is the most effective for preventing bursts from by-passing the rate limit as the window for each limit is not fixed at the start and end of each time unit (i.e. N/second for a moving window means N in the last 1000 milliseconds). There is however a higher memory cost associated with this strategy as it requires N items to be maintained in memory per resource and rate limit.

Configuring Falcon-Limiter

Config values can be provided as a dictionary to the Limiter(). For example:

from falcon_limiter import Limiter
from falcon_limiter.utils import get_remote_addr

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits="5 per minute,2 per second",
    config={
        'RATELIMIT_KEY_PREFIX': 'myapp',
        'RATELIMIT_STORAGE_URL': 'redis://@redis:6379',
        'RATELIMIT_STRATEGY': 'moving-window'
    }
)

The following configuration values exist for Falcon-Limiter:

RATELIMIT_STORAGE_URL

A storage location conforming to the scheme in Supported versions. A basic in-memory storage can be used by specifying memory:// though this should probably never be used in production. Some supported backends include:

  • Memcached: memcached://host:port
  • Memcached on Google App Engine: gaememcached://host:port
  • Redis listening on TCP: redis://host:port
  • Redis listening on a unix domain socket: redis+unix:///path/to/socket?db=n
  • Redis with password and db specified: redis://:password@host:port?db=n
  • Redis over SSL: rediss://host:port
  • Redis Sentinel: redis+sentinel://host:26379/my-redis-service or redis+sentinel://host:26379,host:26380/my-redis-service
  • Redis Cluster: redis+cluster://localhost:7000 or redis+cluster://localhost:7000,localhost:70001
  • GAE Memcached: gaememcached://host:port

For more examples and requirements of supported backends please refer to Supported versions.

RATELIMIT_STORAGE_OPTIONS A dictionary to set extra options to be passed to the storage implementation upon initialization. (Useful if you’re subclassing limits.Storage to create a subclassing limits.storage.Storage to create a custom Storage backend.)
RATELIMIT_STRATEGY The rate limiting strategy to use. See Rate limiting strategies for details.
RATELIMIT_KEY_PREFIX If you are using a shared backend - like a Redis instance shared by multiple apps, then to avoid a potential clash between the ratelimit records, you should provide a string in RATELIMIT_KEY_PREFIX, which will be added to the key.

Recipes

Application is served from behind a reverse proxy

Falcon applications are frequently served from behind loadbalancers and reverse proxies. In such a case care must be given to pick up the right IP address - the one representing the requestor and NOT the reverse proxy, otherwise you will be applying a shared rate limit to all your users coming through that reverse proxy.

Usually the reverse proxy appends the IP address to one of the header (like X-Forwarded-For) and in Falcon the list of IP addresses from those headers are amde available under the access_route Request attribute. See https://falcon.readthedocs.io/en/stable/api/request_and_response.html#falcon.Request.access_route

An example recipe to handle a single reverse proxy in front of our application is to provide a custom key function which derives the requestor’s IP address from the access_route:

import falcon
from falcon_limiter import Limiter

# a custom key function
def get_access_route_addr(req, resp, resource, params) -> str:
    """ Get the requestor's IP by discounting 1 reverse proxy
    """
    return req.access_route[-2]

limiter = Limiter(
    key_func=get_access_route_addr,
    default_limits="5 per minute,2 per second",
)

@limiter.limit()
class ThingsResource:
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

    # this endpoint is routed differently (through 2 proxies), so it
    # requires a custom key function specific to this method
    @limiter.limit(key_func=lambda req, resp, resource, params: req.access_route[-3])
    def on_post(self, req, resp):
        resp.body = 'Hello world!'

Note

A custom key function must accept the ‘usual’ Falcon response attributes and return a string:

def custom_key_func(req, resp, resource, params) -> str:

Ratelimit by resource and method

A custom key function can be useful in other scenarios too, for example when you want to ratelimit by resource and method. This is the scenario where you would want the ratelimit counted SEPARATELY for each endpoint.

def get_key(req, resp, resource, params) -> str:
    """ Build a key from the IP + resource name + method name """
    user_key = get_remote_addr(req, resp, resource, params)
    return f"{user_key}:{resource.__class__.__name__}:{req.method}"

limiter = Limiter(
    key_func=get_key,
    default_limits=["10 per hour", "2 per minute"]
)

Ratelimit by user instead of IP

A custom key function can be also be used to implement rate limit by authenticated user instead of IP. This can be useful in scenarios when the users are coming from a proxied environment (like most corporate environment), as they will be sharing the same public IP.

First you will need to authenticate your user and place the user id onto the request context, so then your custom key function can pick it up from there.

def get_key_(req, resp, resource, params) -> str:
    """ Build a key from the user id stored on the request context
    or the IP when that is user id not available """
    if hasattr(req.context, 'user_id'):
        return req.context.user_id
    else:
        return get_remote_addr(req, resp, resource, params)

limiter = Limiter(
    key_func=get_key,
    default_limits=["10 per hour", "2 per minute"]
)

Dynamic limits

With the use of the default_dynamic_limits and dynamic_limits parameters you can define the limits dynamically, at the time of the processing of the request.

This allows you to define different limits by users - for example allowing an admin user higher limit than others, or differentiating the limits based on the ‘subscription’ the given requester belongs to.

from falcon_limiter.utils import get_remote_addr

limiter = Limiter(
    key_func=get_remote_addr,
    # the default limit is 9999/second for admin and
    # 20/minute,2/second for everybody else:
    default_dynamic_limits=lambda req, resp, resource, params:
        '9999/second' if req.context.user == 'admin'
        else '20/minute,2/second'
)

@limiter.limit()
class ThingsResource:
    # this endpoint gets a 5/second limit for those sending
    the APIUSER=admin header:
    @limiter.limit(dynamic_limits=lambda req, resp, resource, params:
        '5/second'if req.get_header('APIUSER') == 'admin'
        else '20/minute,2/second'
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

    def on_post(self, req, resp):
        resp.body = 'Hello world!'

Customizing rate limits based on response

For scenarios where the decision to count the current request towards a rate limit can only be made after the request has completed, a callable can be provided.

The deduct_when function can be either provided to the Limiter as default_deduct_when parameter or to the decorator as deduct_when parameter.

import falcon
from falcon_limiter import Limiter

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits=["10 per hour", "2 per minute"],
    # this will apply to ALL limits:
    default_deduct_when=lambda req, resp, resource, req_succeeded:
        resp.status == falcon.HTTP_200
)

@limiter.limit()
class ThingsResource:
    # this deduct when only applies to this method
    @limiter.limit(deduct_when=lambda req, resp, resource, req_succeeded:
        resp.status != falcon.HTTP_500)
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

    def on_post(self, req, resp):
        resp.body = 'Hello world!'

Multiple decorators

For scenarios where there is a need for multiple decorators and the @limiter.limit() cannot be the topmost one, we need to register the decorators a special way.

This scenario is complicated because our @limiter.limit() just marks the fact that the given method is decorated with a limit, which later gets picked up by the middleware and triggers the rate limiting. If the @limiter.limit() is the topmost decorator then it is easy to pick that up, but if there are other decorators ‘ahead’ it, then those will ‘hide’ the @limiter.limit(). This is because decorators in Python are just syntactic sugar for nested function calls.

To be able to tell if the given endpoint was decorated by the @limiter.limit() decorator when that is NOT the topmost decorator, you need to decorate your method by registering your decorators using the @register() helper decorator.

See more about this issue at https://stackoverflow.com/questions/3232024/introspection-to-get-decorator-names-on-a-method

import falcon
from falcon_limiter import Limiter
from falcon_limiter.utils import register

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits=["10 per hour", "2 per minute"]
)


class ThingsResource:
    # this is fine, as the @limiter.limit() is the topmost decorator:
    @limiter.limit()
    @another_decorator
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

    # the @limiter.limit() is NOT the topmost decorator, so
    # this would NOT work - the limit would be ignored!!!!
    # DO NOT DO THIS:
    @another_decorator
    @limiter.limit()
    def on_post(self, req, resp):
        resp.body = 'WARNING: NO LIMITS ON THIS!'

    # instead register your decorators this way:
    @register(another_decorator, limiter.limit())
    def on_post(self, req, resp):
        resp.body = 'This is properly limited'

app = falcon.API(middleware=limiter.middleware)

Note

The deduct_when function must accept the ‘usual’ Falcon response attributes and return a boolean:

def my_deduct_when_func(req, resp, resource, params) -> bool:

async (experimental)

New in version 1.0.

Warning

Experimental

Async (ASGI) support has been added in experimental mode, thanks to the Limits library.

This was implemented by the addition of falcon.limiter.AsyncLimiter module, which copies the functionalities of falcon.limiter.Limiter.

Use falcon.limiter.AsyncLimiter for Async (ASGI) and falcon.limiter.Limiter for WSGI.

The following async storage backends are implemented:

Async examples

A few examples to demonstrate the use of the async module.

  1. A basic example using the In-Memory storage option
import falcon.asgi
from falcon_limiter import AsyncLimiter
from falcon_limiter.utils import get_remote_addr

limiter = AsyncLimiter(
    key_func=get_remote_addr,
    default_limits="5 per minute,2 per second"
)

# use the default limit for all methods of this class
@limiter.limit()
class ThingsResource:
    async def on_get(self, req, resp):
        resp.body = 'Hello world!'

# add the limiter middleware to the Falcon app
app = falcon.asgi.App(middleware=limiter.middleware)

things = ThingsResource()
app.add_route('/things', things)
  1. Redis storage
import falcon.asgi
from falcon_limiter import AsyncLimiter
from falcon_limiter.utils import get_remote_addr
import logging

# include ERROR logs
logging.basicConfig()
logging.getLogger().setLevel(logging.ERROR)

limiter = AsyncLimiter(
    key_func=get_remote_addr,
    default_limits=["500 per hour", "20 per minute"],
    config={
        'RATELIMIT_KEY_PREFIX': 'myapp',
        'RATELIMIT_STORAGE_URL': 'async+redis://:MyRedisPassword@localhost:6379'
    }
)

# The deduct_when function is NOT async!
def deduct_when_func(req, resp, resource, req_succeeded):
    return resp.status == falcon.HTTP_200

class ThingsResource:
    @limiter.limit(limits="2 per hour;1 per minute",
                   deduct_when=deduct_when_func)
    async def on_get(self, req, resp):
        resp.body = 'Hello world!'

# add the limiter middleware to the Falcon app
app = falcon.asgi.App(middleware=limiter.middleware)

things = ThingsResource()
app.add_route('/things', things)

Please note, that when using the AsyncLimiter, then a class-level decorator will overwrite all method-level decorators of that class. This behaviour is different from the WSGI (eg sync) Limiter, where method-level decorators overwrite the class level one.

Development

For development guidelines see https://github.com/zoltan-fedor/falcon-limiter#development

API Reference

If you are looking for information on a specific function, class or method of a service, then this part of the documentation is for you.