Skip to content

Rate Limiting

arvel includes a simple, cache-backed rate limiter you can combine with any key to restrict the rate of an action over a window of time — most commonly to throttle inbound HTTP requests, but usable for anything (an outbound API call, a costly job).

The rate limiter is backed by the cache. A cache-less app falls back to a process-global window (fine for a single running process; use a shared cache-backed store in production).

Basic usage

Resolve the limiter from the container (or the RateLimiter facade) and drive it by a key you choose:

from arvel.support.facades import RateLimiter

async def send_message(user):
    key = f"send-message:{user.id}"
    if await RateLimiter.too_many_attempts(key, max_attempts=5):
        seconds = await RateLimiter.available_in(key)
        abort(429, f"Try again in {seconds}s")
    await RateLimiter.hit(key, decay_seconds=60)     # count this attempt; window = 60s
    # ... send the message

Other useful methods:

await RateLimiter.attempts(key)                 # attempts made so far
await RateLimiter.remaining(key, max_attempts)  # attempts left
await RateLimiter.clear(key)                    # reset the counter

attempt() combines the check-and-hit in one call, running a callback only when there's budget left.

Named limiters for routes

Define a named limiter once — usually in a service provider's boot — with a resolver that returns a Limit, then apply it to routes with the throttle:<name> middleware string:

from arvel.support.facades import RateLimiter
from arvel.http.rate_limiter import Limit

RateLimiter.for_("api", lambda request: Limit.per_minute(60).by(request.ip()))
Route.get("/api/posts", index).middleware("throttle:api")

By default a request over the limit gets a 429 with Retry-After and X-RateLimit-* headers — unless the limit sets its own .response(...) (below), which replaces that default entirely.

Building limits

Limit is a small fluent builder:

Limit.per_second(10)
Limit.per_minute(60)
Limit.per_hour(1000)
Limit.per_day(10_000)
Limit.per_minute(60).by(request.ip())            # segment the window by a key
Limit.per_minute(5).response(custom_429_handler) # custom over-limit response

.by(key) sets the segment explicitly (per IP, per user, per tenant). Without it, the default segment is the authenticated user's id, falling back to the client IP — so callers already get independent windows; use .by(...) when you want to key on something else.

See also

  • Middleware — how throttle:<name> is resolved and applied.
  • Cache — the store the limiter counts in.
  • Requestsrequest.ip() and other segmenting keys.