Skip to content

Rate limiting

Shortly after you deploy your app in production users will at best misuse it and at worse attack it. It is worthwhile then being defensive from the outset by adding rate limiting, which limits the rate at which a remote client can make requests to the app. We'll use the Quart extension Quart-Rate-Limiter to enforce rate limits, first by installing,

Run this command in backend/

poetry add quart-rate-limiter

which installed 0.4.0. Then by activating the RateLimiter when creating the app in backend/src/backend/run.py,

from quart_rate_limiter import RateLimiter

rate_limiter = RateLimiter()

def create_app() -> None:
    ...
    rate_limiter.init_app(app)
    ...

With this any route in the app can be given rate limit protection, for example to limit to 6 requests per minute,

from datetime import timedelta

from quart_rate_limiter import rate_limit

@blueprint.route("/")
@rate_limit(6, timedelta(minutes=1))
async def handler():
    ...

It would be useful to provide a JSON response if the client exceeds the rate limit, we can do this by adding the following error handler to be backend/src/backend/run.py

from quart_rate_limiter import RateLimitExceeded

def create_app() -> Quart:
    ...

    @app.errorhandler(RateLimitExceeded)
    async def handle_rate_limit_exceeded_error(
        error: RateLimitExceeded,
    ) -> ResponseReturnValue:
        return {}, error.get_headers(), 429

Testing

I like to check that all routes have rate limits or are marked as exempt using the rate_exempt decorator. To do this I add the following to tests/test_rate_limits.py,

from quart import Quart
from quart_rate_limiter import (
    QUART_RATE_LIMITER_EXEMPT_ATTRIBUTE, QUART_RATE_LIMITER_LIMITS_ATTRIBUTE
)

def test_routes_have_rate_limits(app: Quart) -> None:
    for rule in app.url_map.iter_rules():
        endpoint = rule.endpoint
        if endpoint == "static":
            continue

        exempt = getattr(
            app.view_functions[endpoint],
            QUART_RATE_LIMITER_EXEMPT_ATTRIBUTE,
            False,
        )
        if not exempt:
            rate_limits = getattr(
                app.view_functions[endpoint],
                QUART_RATE_LIMITER_LIMITS_ATTRIBUTE,
                [],
            )
            assert rate_limits != []

For this to pass will also need to add the rate_exempt decorator to the control ping endpoint in backend/src/backend/blueprints/control.py,

...
from quart_rate_limiter import rate_exempt

@blueprint.route("/control/ping/")
@rate_exempt
async def ping() -> ResponseReturnValue:
    return {"ping": "pong"}