Skip to content

Rate Limiting Reference

Full API reference for @rate_limit, rate limit models, engine methods, and CLI commands.


@rate_limit decorator

from shield.fastapi.decorators import rate_limit

Declares a rate limit policy on a route. The policy is registered by ShieldRouter at startup and enforced by ShieldMiddleware on every matching request.

@router.get("/public/posts")
@rate_limit("10/minute")
async def list_posts():
    ...

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-routeresponse= 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 defaultresponses["rate_limited"] on ShieldMiddleware:

app.add_middleware(
    ShieldMiddleware,
    engine=engine,
    responses={"rate_limited": my_429},
)

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

from shield.core.rate_limit.models import 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

from shield.core.rate_limit.models import 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

from shield.core.rate_limit.models import 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

from shield.core.rate_limit.models import 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

from shield.core.rate_limit.models import 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

from shield.core.rate_limit.models import 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

from shield.core.rate_limit.models import 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).

await engine.set_rate_limit_policy(
    "/public/posts", "GET", "20/minute", actor="alice"
)

delete_rate_limit_policy

async def delete_rate_limit_policy(
    path: str,
    method: str,
    actor: str = "system",
) -> None

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.

await engine.delete_rate_limit_policy("/public/posts", "GET", actor="alice")

reset_rate_limit

async def reset_rate_limit(
    path: str,
    method: str | None = None,
    actor: str = "system",
) -> None

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

async def get_rate_limit_hits(
    path: str | None = None,
    limit: int = 100,
) -> list[RateLimitHit]

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

async def list_rate_limit_policies() -> list[RateLimitPolicy]

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

from shield.core.rate_limit.models import 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

async def get_global_rate_limit() -> GlobalRateLimitPolicy | None

Return the current policy, or None if not configured.


delete_global_rate_limit

async def delete_global_rate_limit(*, actor: str = "system") -> None

Remove the global rate limit policy entirely. Logged as global_rl_deleted.


reset_global_rate_limit

async def reset_global_rate_limit(*, actor: str = "system") -> None

Clear all global counters so the limit starts fresh. The policy itself is not removed. Logged as global_rl_reset.


enable_global_rate_limit

async def enable_global_rate_limit(*, actor: str = "system") -> None

Resume a paused global rate limit policy. No-op if already enabled or not configured. Logged as global_rl_enabled.


disable_global_rate_limit

async def disable_global_rate_limit(*, actor: str = "system") -> None

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

async def get_service_rate_limit(service: str) -> GlobalRateLimitPolicy | None

Return the current policy for a service, or None if not configured.


delete_service_rate_limit

async def delete_service_rate_limit(service: str, *, actor: str = "system") -> None

Remove the service rate limit policy entirely. Logged as svc_rl_deleted.


reset_service_rate_limit

async def reset_service_rate_limit(service: str, *, actor: str = "system") -> None

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

async def enable_service_rate_limit(service: str, *, actor: str = "system") -> None

Resume a paused service rate limit policy. No-op if already enabled or not configured. Logged as svc_rl_enabled.


disable_service_rate_limit

async def disable_service_rate_limit(service: str, *, actor: str = "system") -> None

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          # short form
shield rate-limits list # identical

shield rl list

Show all registered rate limit policies.

shield rl list

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 <route> <limit>
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 reset GET:/public/posts

shield rl delete

Remove a persisted policy override from the backend.

shield rl delete GET:/public/posts

shield rl hits

Show the blocked requests log.

shield rl hits                    # last 20 entries
shield rl hits --limit 50         # show more
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 current policy
shield global-rate-limit get  # identical

shield grl get

Show the current global rate limit policy (limit, algorithm, key strategy, burst, exempt routes, enabled state).

shield grl get

shield grl set

Configure the global rate limit. Creates a new policy or replaces the existing one.

shield grl set <limit>
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 delete

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 reset

shield grl enable

Resume a paused global rate limit policy.

shield grl enable

shield grl disable

Pause the global rate limit without removing it. Per-route policies continue to enforce normally.

shield grl disable

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 payments-service
shield service-rate-limit get payments-service   # identical

shield srl get

Show the current rate limit policy for a service.

shield srl get <service>
shield srl get payments-service

shield srl set

Configure the rate limit for a service. Creates a new policy or replaces the existing one.

shield srl set <service> <limit>
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 delete <service>
shield srl delete payments-service

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 reset <service>
shield srl reset payments-service

shield srl enable

Resume a paused service rate limit policy.

shield srl enable <service>

shield srl disable

Pause the service rate limit without removing it. Per-route policies continue to enforce normally.

shield srl disable <service>

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:

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1748736120

Blocked requests additionally receive:

HTTP/1.1 429 Too Many Requests
Retry-After: 23