Rate Limiting Reference¶
Full API reference for @rate_limit, rate limit models, engine methods, and CLI commands.
@rate_limit decorator¶
Declares a rate limit policy on a route. The policy is registered by ShieldRouter at startup and enforced by ShieldMiddleware on every matching request.
Signature¶
def rate_limit(
limit: str | dict[str, str],
*,
algorithm: str = "fixed_window",
key: str | Callable = "ip",
on_missing_key: str | None = None,
burst: int = 0,
tier_resolver: str = "plan",
exempt_ips: list[str] | None = None,
exempt_roles: list[str] | None = None,
) -> Callable
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
str \| dict |
required | Limit string ("100/minute") or tier dict ({"free": "10/min", "pro": "100/min"}) |
algorithm |
str |
"fixed_window" |
Counting algorithm. One of: fixed_window, sliding_window, moving_window, token_bucket |
key |
str \| callable |
"ip" |
Key strategy. One of: "ip", "user", "api_key", "global", or a sync/async callable (Request) -> str \| None |
on_missing_key |
str \| None |
strategy default | Behaviour when the key extractor returns None. One of: "exempt", "fallback_ip", "block" |
burst |
int |
0 |
Extra requests allowed above limit (additive) |
tier_resolver |
str |
"plan" |
request.state attribute name used to look up the caller's tier. Only applies when limit is a dict. |
exempt_ips |
list[str] \| None |
[] |
IP addresses or CIDR ranges that bypass the limit entirely |
exempt_roles |
list[str] \| None |
[] |
Roles (from request.state.user_roles) that bypass the limit entirely |
response |
callable \| None |
None |
Custom response factory for rate limit violations. See Custom responses. |
Custom responses¶
Replace the default 429 JSON body with any Starlette Response.
Per-route — response= on the decorator:
from starlette.requests import Request
from starlette.responses import JSONResponse
from shield.core.exceptions import RateLimitExceededException
def my_429(request: Request, exc: RateLimitExceededException) -> JSONResponse:
return JSONResponse(
{"ok": False, "retry_after": exc.retry_after_seconds},
status_code=429,
headers={"Retry-After": str(exc.retry_after_seconds)},
)
@router.get("/posts")
@rate_limit("10/minute", response=my_429)
async def list_posts(): ...
Global default — responses["rate_limited"] on ShieldMiddleware:
Resolution order: per-route response= → responses["rate_limited"] → built-in 429 JSON.
Factory signature:
def factory(request: Request, exc: RateLimitExceededException) -> Response: ...
# or async:
async def factory(request: Request, exc: RateLimitExceededException) -> Response: ...
Useful exc attributes: limit, retry_after_seconds, reset_at, remaining, key.
Dependency injection¶
@rate_limit works as a Depends() dependency. The engine is resolved from request.app.state.shield_engine (set automatically by ShieldMiddleware).
from fastapi import Depends
from shield.fastapi.decorators import rate_limit
@router.get("/export", dependencies=[Depends(rate_limit("5/hour", key="user"))])
async def export(): ...
Both the decorator path and the Depends() path use the same counter — they are equivalent in enforcement.
RateLimitAlgorithm¶
Controls how requests are counted within a window.
| Value | Description |
|---|---|
FIXED_WINDOW |
Fixed time buckets. Simple and predictable. The default. Allows boundary bursts (up to 2x in the worst case). |
SLIDING_WINDOW |
Blends two adjacent fixed-window counters. Smooths boundary bursts. Not suitable for small limits like 5/minute where gradual re-allow looks like intermittent blocking. |
MOVING_WINDOW |
Timestamps every individual request. Most accurate; highest memory. |
TOKEN_BUCKET |
Tokens accumulate over time up to a cap. Good for controlled bursts with a sustained average rate. Currently mapped to MOVING_WINDOW — a native implementation will be used when available from the limits library. |
RateLimitKeyStrategy¶
Controls what value is used as the bucket key for each request.
| Value | Key source | Never missing? | Default on_missing_key |
|---|---|---|---|
IP |
X-Forwarded-For, X-Real-IP, or ASGI scope |
Yes (falls back to "unknown") |
N/A |
USER |
request.state.user_id |
No | EXEMPT |
API_KEY |
X-API-Key header |
No | FALLBACK_IP |
GLOBAL |
Route path (shared by all callers) | Yes | N/A |
CUSTOM |
Sync or async callable provided by the caller | No | EXEMPT |
OnMissingKey¶
Controls what happens when the configured key strategy cannot extract a key from the request.
| Value | Behaviour |
|---|---|
EXEMPT |
Skip the rate limit entirely. No counter is incremented. The response is returned normally with no rate-limit headers. |
FALLBACK_IP |
Use the client IP as the key. The request is rate limited, just bucketed by IP rather than the original strategy. |
BLOCK |
Return 429 immediately without incrementing any counter. |
The default per strategy is documented in RateLimitKeyStrategy above. Override it with on_missing_key= on the decorator.
RateLimitPolicy¶
Full rate limiting policy for a single route + method combination. Registered by ShieldRouter and stored in the backend.
class RateLimitPolicy(BaseModel):
path: str
method: str
limit: str
algorithm: RateLimitAlgorithm = RateLimitAlgorithm.FIXED_WINDOW
key_strategy: RateLimitKeyStrategy = RateLimitKeyStrategy.IP
on_missing_key: OnMissingKey | None = None
burst: int = 0
tiers: list[RateLimitTier] = []
tier_resolver: str = "plan"
exempt_ips: list[str] = []
exempt_roles: list[str] = []
RateLimitTier¶
A named tier for tiered rate limiting.
class RateLimitTier(BaseModel):
name: str # matched against request.state.<tier_resolver>
limit: str # e.g. "100/minute" or "unlimited"
RateLimitResult¶
Result of a single rate limit check. Read by the middleware to build the response.
class RateLimitResult(BaseModel):
allowed: bool
limit: str
remaining: int
reset_at: datetime
retry_after_seconds: int # 0 when allowed
key: str # the actual key used
tier: str | None # which tier was applied, if any
key_was_missing: bool
missing_key_behaviour: OnMissingKey | None
RateLimitHit¶
Record of a single blocked request. Written to the backend on every 429 response.
class RateLimitHit(BaseModel):
id: str # UUID4
timestamp: datetime
path: str
method: str
key: str # the key that exceeded the limit
limit: str
tier: str | None
reset_at: datetime
The log is capped at max_rl_hit_entries (default 10_000) — oldest entries are evicted when the cap is reached.
Engine methods¶
set_rate_limit_policy¶
async def set_rate_limit_policy(
path: str,
method: str,
limit: str,
algorithm: str = "fixed_window",
key_strategy: str = "ip",
burst: int = 0,
actor: str = "system",
platform: str = "",
) -> RateLimitPolicy
Register or update a rate limit policy at runtime. Persists to the backend so other instances and restarts pick it up. The change is logged in the audit log with action rl_policy_set (new) or rl_policy_updated (existing policy replaced).
delete_rate_limit_policy¶
Remove a persisted policy override. If the route has a @rate_limit(...) decorator, the decorator's original policy remains active in memory but is no longer stored. Logged with action rl_policy_deleted.
reset_rate_limit¶
Clear all rate limit counters for a route. When method is omitted, counters for all methods on the path are cleared. Logged with action rl_reset.
await engine.reset_rate_limit("/public/posts", "GET", actor="alice")
await engine.reset_rate_limit("/public/posts") # all methods
get_rate_limit_hits¶
Return blocked request records, newest first. Optionally filter by route path.
hits = await engine.get_rate_limit_hits(limit=50)
hits = await engine.get_rate_limit_hits(path="/public/posts")
list_rate_limit_policies¶
Return all registered rate limit policies.
policies = await engine.list_rate_limit_policies()
for p in policies:
print(p.method, p.path, p.limit)
Global rate limit¶
A global rate limit applies a single policy across all routes with higher precedence than per-route limits. It is checked first on every request — if the global limit is exceeded the request is rejected immediately and the per-route counter is never touched. Per-route policies only run after the global limit passes (or when the route is exempt, or no global limit is configured).
GlobalRateLimitPolicy¶
| Field | Type | Default | Description |
|---|---|---|---|
limit |
str |
required | Limit string, e.g. "1000/minute" |
algorithm |
str |
"fixed_window" |
Counting algorithm |
key_strategy |
str |
"ip" |
Key strategy: ip, user, api_key, global |
on_missing_key |
str \| None |
strategy default | Behaviour when the key extractor returns None |
burst |
int |
0 |
Extra requests allowed above limit |
exempt_routes |
list[str] |
[] |
Routes skipped by the global limit. Bare path ("/health") exempts all methods; method-prefixed ("GET:/metrics") exempts that method only |
enabled |
bool |
True |
Whether the policy is currently enforced. False = paused (policy kept, counters not incremented) |
Engine methods¶
set_global_rate_limit¶
async def set_global_rate_limit(
limit: str,
*,
algorithm: str | None = None,
key_strategy: str | None = None,
on_missing_key: str | None = None,
burst: int = 0,
exempt_routes: list[str] | None = None,
actor: str = "system",
platform: str = "",
) -> GlobalRateLimitPolicy
Create or replace the global rate limit policy. Persists to the backend. Logged as global_rl_set (new) or global_rl_updated (replacement).
await engine.set_global_rate_limit(
"1000/minute",
key_strategy="ip",
exempt_routes=["/health", "GET:/metrics"],
actor="alice",
)
get_global_rate_limit¶
Return the current policy, or None if not configured.
delete_global_rate_limit¶
Remove the global rate limit policy entirely. Logged as global_rl_deleted.
reset_global_rate_limit¶
Clear all global counters so the limit starts fresh. The policy itself is not removed. Logged as global_rl_reset.
enable_global_rate_limit¶
Resume a paused global rate limit policy. No-op if already enabled or not configured. Logged as global_rl_enabled.
disable_global_rate_limit¶
Pause the global rate limit without removing it. Requests are no longer counted or blocked by the global policy; per-route policies are unaffected. Logged as global_rl_disabled.
Dashboard¶
The Rate Limits page includes a Global Rate Limit card above the policies table.
- Not configured — compact bar with a "Set Global Limit" button.
- Active — info card showing limit, algorithm, key strategy, burst, and exempt routes. Action buttons: Pause, Edit, Reset, Remove.
- Paused — same card with a "Paused" badge (grey) and a Resume button instead of Pause. The limit string is dimmed to indicate it is not being enforced.
Per-service rate limit¶
A per-service rate limit applies a GlobalRateLimitPolicy to all routes of one service. It sits between the all-services global rate limit and per-route limits in the enforcement chain:
global maintenance -> service maintenance -> global rate limit -> service rate limit -> per-route rate limit
Uses the same GlobalRateLimitPolicy model as the all-services global rate limit. The policy is persisted in the backend via a sentinel key so it survives restarts.
Engine methods¶
set_service_rate_limit¶
async def set_service_rate_limit(
service: str,
limit: str,
*,
algorithm: str | None = None,
key_strategy: str | None = None,
on_missing_key: str | None = None,
burst: int = 0,
exempt_routes: list[str] | None = None,
actor: str = "system",
platform: str = "",
) -> GlobalRateLimitPolicy
Create or replace the rate limit policy for a service. Persists to the backend. Logged as svc_rl_set (new) or svc_rl_updated (replacement).
await engine.set_service_rate_limit(
"payments-service",
"1000/minute",
key_strategy="ip",
exempt_routes=["/health", "GET:/metrics"],
actor="alice",
)
get_service_rate_limit¶
Return the current policy for a service, or None if not configured.
delete_service_rate_limit¶
Remove the service rate limit policy entirely. Logged as svc_rl_deleted.
reset_service_rate_limit¶
Clear all counters for the service so the limit starts fresh. The policy is not removed. Logged as svc_rl_reset.
enable_service_rate_limit¶
Resume a paused service rate limit policy. No-op if already enabled or not configured. Logged as svc_rl_enabled.
disable_service_rate_limit¶
Pause the service rate limit without removing it. Per-route policies are unaffected. Logged as svc_rl_disabled.
Dashboard¶
When a service filter is active on the Rate Limits page (/shield/rate-limits?service=<name>), a Service Rate Limit card appears between the global RL card and the policies table.
- Not configured — compact bar with a "Set Service Limit" button.
- Active — info card showing limit, algorithm, key strategy, burst, and exempt routes. Action buttons: Pause, Edit, Reset, Remove.
- Paused — same card with a "Paused" badge and a Resume button.
CLI commands¶
shield rl and shield rate-limits are aliases for the same command group — use whichever you prefer.
shield rl list¶
Show all registered rate limit policies.
Output:
| Route | Limit | Algorithm | Key Strategy |
|---|---|---|---|
| GET /public/posts | 10/minute | fixed_window | ip |
| GET /search | 5/minute | fixed_window | global |
| GET /users/me | 100/minute | fixed_window | user |
shield rl set¶
Register or update a policy at runtime. Changes take effect on the next request.
shield rl set GET:/public/posts 20/minute
shield rl set GET:/public/posts 5/second --algorithm fixed_window
shield rl set GET:/search 10/minute --key global
| Option | Description |
|---|---|
--algorithm TEXT |
Counting algorithm: fixed_window, sliding_window, moving_window, token_bucket |
--key TEXT |
Key strategy: ip, user, api_key, global |
shield rl reset¶
Clear all rate limit counters for a route immediately. Clients get their full quota back on the next request.
shield rl delete¶
Remove a persisted policy override from the backend.
shield rl hits¶
Show the blocked requests log.
| Option | Description |
|---|---|
--limit INT |
Maximum entries to display (default: 20) |
shield grl / shield global-rate-limit¶
shield grl and shield global-rate-limit are aliases for the global rate limit command group.
shield grl get¶
Show the current global rate limit policy (limit, algorithm, key strategy, burst, exempt routes, enabled state).
shield grl set¶
Configure the global rate limit. Creates a new policy or replaces the existing one.
shield grl set 1000/minute
shield grl set 500/minute --algorithm sliding_window --key ip
shield grl set 2000/hour --burst 50 --exempt /health --exempt GET:/metrics
| Option | Description |
|---|---|
--algorithm TEXT |
Counting algorithm: fixed_window, sliding_window, moving_window, token_bucket |
--key TEXT |
Key strategy: ip, user, api_key, global |
--burst INT |
Extra requests above the base limit |
--exempt TEXT |
Exempt route (repeatable). Bare path or METHOD:/path |
shield grl delete¶
Remove the global rate limit policy entirely.
shield grl reset¶
Clear all global rate limit counters. The policy is kept; clients get their full quota back on the next request.
shield grl enable¶
Resume a paused global rate limit policy.
shield grl disable¶
Pause the global rate limit without removing it. Per-route policies continue to enforce normally.
shield srl / shield service-rate-limit¶
shield srl and shield service-rate-limit are aliases for the per-service rate limit command group. Requires api-shield[rate-limit] on the server.
shield srl get¶
Show the current rate limit policy for a service.
shield srl set¶
Configure the rate limit for a service. Creates a new policy or replaces the existing one.
shield srl set payments-service 1000/minute
shield srl set payments-service 500/minute --algorithm sliding_window --key ip
shield srl set payments-service 2000/hour --burst 50 --exempt /health --exempt GET:/metrics
| Option | Description |
|---|---|
--algorithm TEXT |
Counting algorithm: fixed_window, sliding_window, moving_window, token_bucket |
--key TEXT |
Key strategy: ip, user, api_key, global |
--burst INT |
Extra requests above the base limit |
--exempt TEXT |
Exempt route (repeatable). Bare path (/health) or method-prefixed (GET:/metrics) |
shield srl delete¶
Remove the service rate limit policy entirely.
shield srl reset¶
Clear all counters for the service. The policy is kept; clients get their full quota back on the next request.
shield srl enable¶
Resume a paused service rate limit policy.
shield srl disable¶
Pause the service rate limit without removing it. Per-route policies continue to enforce normally.
Audit log integration¶
Rate limit policy changes are recorded in the same audit log as route state changes. The action field uses the following values:
Per-route:
| Action | Badge | When |
|---|---|---|
rl_policy_set |
set | New per-route policy registered |
rl_policy_updated |
update | Existing per-route policy replaced |
rl_reset |
reset | Per-route counters cleared |
rl_policy_deleted |
delete | Per-route policy removed |
Global (all services):
| Action | Badge | When |
|---|---|---|
global_rl_set |
global set | Global policy created |
global_rl_updated |
global update | Global policy replaced |
global_rl_reset |
global reset | Global counters cleared |
global_rl_deleted |
global delete | Global policy removed |
global_rl_enabled |
global enabled | Policy resumed after pause |
global_rl_disabled |
global disabled | Policy paused |
Per-service:
| Action | Badge | When |
|---|---|---|
svc_rl_set |
svc set | Service policy created |
svc_rl_updated |
svc update | Service policy replaced |
svc_rl_reset |
svc reset | Service counters cleared |
svc_rl_deleted |
svc delete | Service policy removed |
svc_rl_enabled |
svc enabled | Service policy resumed after pause |
svc_rl_disabled |
svc disabled | Service policy paused |
The Path column for service rate limit entries displays as [{service} Rate Limit] (e.g. [payments-service Rate Limit]).
View in the dashboard at /shield/audit or via shield log.
Response headers¶
Every request to a rate-limited route (allowed or blocked) receives:
Blocked requests additionally receive: