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()))
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.
- Requests —
request.ip()and other segmenting keys.