Configuration¶
Choosing an algorithm¶
| Token bucket | Sliding window | Fixed window | |
|---|---|---|---|
| Burst tolerance | Yes — up to burst capacity, refilling continuously |
No inherent burst beyond smoothing boundary effects | No — allows up to 2x at window boundaries |
| Precision | Exact | Approximate (weighted estimate) | Exact |
| Memory per identity | One Redis Hash (2 fields) | Two Redis strings (current + previous window) | One Redis string |
| Best for | APIs where occasional bursts are fine but sustained abuse isn't | General-purpose limiting without fixed-window's boundary problem | Simplest, cheapest; fine when boundary bursts don't matter |
Default to token_bucket — it's the most flexible (burst lets you tune
exactly how much bursting is tolerated) and matches how most real API
rate limiters (including Stripe's and GitHub's) actually behave.
rate_limit(rate="100/m", algorithm="token_bucket", burst=20)
Use sliding_window if you specifically want to avoid token bucket's
"tokens accumulate while idle" property (e.g. you want a stricter,
smoothed average rather than any burst allowance):
rate_limit(rate="100/m", algorithm="sliding_window")
Use fixed_window only when you've confirmed the boundary-burst
possibility doesn't matter for your use case (e.g. a very generous limit
where 2x for a moment is inconsequential) — it's marginally cheaper.
Choosing a key function¶
rate_limit(key="ip") # anonymous clients, or IP-based abuse prevention
rate_limit(key="user") # per authenticated user (falls back to IP if anonymous)
rate_limit(key="api_key") # per API key header
rate_limit(key="tenant") # per tenant attribute on the request
rate_limit(key="view") # per endpoint, shared across all callers
Or compose dimensions:
from drf_ratelimit_plus import combine
rate_limit(key=combine("tenant", "user")) # per-user, scoped within each tenant
See docs/api-reference.md for the full list and
how to write a custom key function.
Choosing a scope¶
By default, each view gets an independent limit (the scope defaults to
the view's dotted class path). Pass scope= explicitly to share one limit
across multiple views:
rate_limit(rate="1000/h", scope="expensive-operations")
Weighted costs¶
rate_limit(rate="1000/h", cost=5) # every request costs 5
rate_limit(rate="1000/h", cost=lambda request: len(request.data.get("items", [])))
Plan tiers¶
rate_limit(
rate={"free": "100/h", "pro": "1000/h", "enterprise": "10000/h"},
tier_resolver=lambda request: request.user.subscription.plan_name,
)
See Quick Start.
Dynamic configuration¶
Any of rate, key, or cost can be a callable, re-evaluated on every
request — e.g. reading a database-configured limit:
def dynamic_rate(request):
return RateLimitConfig.objects.get(endpoint=request.path).rate_string
rate_limit(rate=dynamic_rate)
There is no restart required to change behavior this way — the callable runs fresh on every request.