Advanced Features¶
If you are here, you should already know the basics: setup your backend, create a throttle, attach it to a route, done. That gets you surprisingly far, but Traffik has a deeper toolkit for the situations where the "one size fits all" doesn't quite fit.
This section is for when you're ready to graduate from "100/min" to something that actually mirrors how your API works in the real world.
What's in Here¶
Request Costs¶
Not every request deserves the same weight on the rate limit scale. A health check is a feather. A file upload is a boulder. Request costs let you reflect that asymmetry directly in how quota is consumed.
# A bulk export endpoint that counts as 10 requests worth of quota
throttle = HTTPThrottle("export", rate="100/min", cost=10)
Multiple Rate Limits¶
Production APIs rarely need just one limit. You need a strict burst cap and a generous hourly envelope. Stack throttles to enforce both simultaneously, with a clear failure model.
burst = HTTPThrottle("api:burst", rate="20/min")
hourly = HTTPThrottle("api:sustained", rate="500/hour")
# Both must pass. First failure wins
@app.get("/api/data", dependencies=[Depends(burst), Depends(hourly)])
async def get_data():
...
Learn about layered throttling →
Throttle Rules & Wildcards¶
A throttle applies globally by default - every request that hits the router it's attached to. Rules let it ask "do I even apply to this connection?" first, before touching the backend or computing an identifier, so one throttle can target a specific path, method, or condition instead of everything.
from traffik.registry import Rule
# Only throttle GET requests to /api/users
rule = Rule(path="/api/users", methods={"GET"})
throttle = HTTPThrottle("api:users", rate="500/min", rules={rule})
Exemptions¶
Some clients should never be throttled, such as your internal services, premium users, admin tokens, and whitelisted IPs. The EXEMPTED sentinel lets you carve out those exceptions cleanly, with zero overhead.
from traffik import EXEMPTED
async def tiered_identifier(request: Request):
if is_admin_token(request.headers.get("x-admin-token")):
return EXEMPTED # Admin? Go right through.
return request.client.host # Everyone else: throttled by IP.
Context-Aware Backends¶
Building a multi-tenant SaaS? Different tenants likely need different backends. Enterprise tenants get their own Redis instance, free-tier users share one pool. The dynamic_backend=True flag makes the throttle resolve which backend to use on every request instead of locking in one backend at startup.
throttle = HTTPThrottle(
"api:quota",
rate="1000/hour",
dynamic_backend=True, # Resolved per-request from context
)
Learn about context-aware backends →
Response Headers¶
A 429 with no explanation leaves clients flying blind. They will have no idea how many requests they have left, when the window resets, or when it's safe to retry. Traffik computes standard rate-limit header values (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) for you to attach to a response however you see fit; it doesn't inject them automatically.
from traffik import DEFAULT_HEADERS_ALWAYS, DEFAULT_HEADERS_THROTTLED
throttle = HTTPThrottle("api:data", rate="100/min", headers=DEFAULT_HEADERS_ALWAYS)
Learn about response headers →
Custom Throttled Handlers¶
The default 429-or-JSON-message behavior is fine for most apps, but not all. Maybe you want an exact Retry-After timestamp, a specific WebSocket message shape, or a response body that matches the rest of your API's error format. A throttled handler replaces the default entirely.
async def handler(connection, wait_ms, throttle, context):
return JSONResponse({"error": "slow down", "retry_in_ms": wait_ms}, status_code=429)
throttle = HTTPThrottle("api:data", rate="100/min", handle_throttled=handler)
Learn about custom throttled handlers →
Skip Handler¶
Normally, a throttled request never reaches your route. The handler takes over and that's the end of it. skip_handler=True turns that off but state still updates and wait_ms still gets computed exactly as normal, but your own code decides what response to send, instead of the handler.
throttle = HTTPThrottle("api:reports", rate="10/min", skip_handler=True)
@app.get("/reports")
async def get_reports(request: Request):
await throttle(request)
if is_throttled(request):
return cached_report() # degrade instead of failing outright
return generate_report()
Strategy Statistics¶
Sometimes you want to look at a rate limit counter without touching it, e.g, for a X-RateLimit-Remaining header, a /usage endpoint, or feeding a metrics system. throttle.stat(...) reads the current state from the backend and never consumes quota.
Quota Context (Deferred Throttling)¶
Standard throttling is optimistic. Quota is consumed first, and work happens after. That's wrong when the work might fail (don't want to charge quota for nothing) or when several throttles need to agree before anything is consumed at all. QuotaContext defers consumption until you explicitly commit it.
from fastapi import FastAPI, Request, Depends
from traffik import HTTPThrottle
throttle = HTTPThrottle("api:reports", rate="50/hour")
@app.post("/reports/generate")
async def generate_report(request: Request):
async with throttle.quota(request) as ctx:
report = await do_expensive_work() # only consume quota if this succeeds
await ctx.apply()
return report
Throttle Registry¶
Every throttle belongs to a ThrottleRegistry. The registry is the coordination layer that tracks which throttles are active, holds the rules that gate them, and lets you disable or re-enable throttles at runtime (a maintenance mode switch, a feature flag) without touching route code.
from traffik.registry import ThrottleRegistry
registry = ThrottleRegistry()
registry.disable_all() # e.g. during a maintenance window
When Do You Need These?¶
| You want to... | Feature to use |
|---|---|
| Charge more quota for expensive operations | Request Costs |
| Enforce burst + sustained limits together | Multiple Rate Limits |
| Target a throttle at specific paths, methods, or conditions | Rules |
| Let admins or premium users bypass throttling | Exemptions |
| Give each tenant isolated rate limit counters | Context-Aware Backends |
| Tell clients how many requests they have left | Response Headers |
| Customize what happens when a client is throttled | Custom Throttled Handlers |
| Let your own code decide the response, not the default handler | Skip Handler |
| Read rate limit state without consuming quota | Statistics |
| Only consume quota if the work actually succeeds | Quota Context |
| Disable or re-enable throttles at runtime | Throttle Registry |
You can combine all of these
These features compose neatly. A dynamic-backend throttle can have per-request costs and an identifier that returns EXEMPTED for admin tokens. Stack them as your use case demands.