Area 08 // BackendLesson 135 / 14013 min read
Rate limiting
Rate limiting caps how many requests (or tokens) each user, API key or tenant can make in a time window, protecting your API, your budget and your upstream LLM quota, with algorithms such as fixed windows, sliding windows and token buckets, and a clear 429 response when the limit is hit.
Think of it like this
Analogy
A popular museum lets in a fixed number of visitors per time slot, per ticket. A simple rule ("50 per hour, counted by the clock") lets a crowd of 50 enter at 1:59 and another 50 at 2:00. Smarter rules count the last 60 minutes, or hand out entry tokens at a steady rate. And when you're turned away, a good doorman tells you when to come back.
The problem it solves
Without limits, one user's script, a bug in a client, or an attacker can flood your API, slow it for everyone, exhaust your provider's rate limit and run up a huge model bill (denial of wallet). Limits must be fair, shared across all your servers, and communicated clearly so well-behaved clients can back off.
How it works, step by step
- Decide what to limit and by what key: requests per minute and tokens or dollars per day, keyed by user, API key or tenant; IP-based limits as an extra layer for anonymous traffic.
- Pick an algorithm:
- Fixed window: count per calendar window; simple, but allows bursts at window edges (see the code).
- Sliding window: count requests in the last N seconds (exact log, or an approximation from two fixed windows).
- Token bucket: a bucket refills at a steady rate; each request takes a token; allows short bursts up to the bucket size (provider rate limits use similar ideas).
- Concurrency limits: maximum in-flight requests per user (useful for long streaming calls).
- Store counters centrally in Redis with atomic operations (INCR with expiry, or a small Lua script), so all replicas agree (Redis).
- Respond clearly:
429 Too Many Requests,Retry-After, and headers showing limit and remaining (HTTP status codes). - Layer limits: CDN or edge protection, API gateway limits, per-route limits (expensive endpoints stricter), per-plan quotas, and daily token budgets.
- Monitor: 429 rates per route and client; alert on spikes (abuse) and on legitimate users hitting limits often (limits too tight).
See it in code
- Needs
- Nothing: plain Python 3. In production the counters live in Redis so all servers share them.
Fixed vs sliding windows on a burst at a window boundary, and the headers a client receives.
from collections import defaultdict, deque
LIMIT, WINDOW = 5, 60 # 5 requests per 60 seconds per API key
# FIXED WINDOW: count per calendar minute. Simple, but allows bursts at the boundary.
fixed = defaultdict(int)
def fixed_window(key: str, t: float) -> bool:
bucket = (key, int(t // WINDOW))
fixed[bucket] += 1
return fixed[bucket] <= LIMIT
# SLIDING WINDOW LOG: count requests in the last 60 seconds exactly.
logs = defaultdict(deque)
def sliding_window(key: str, t: float) -> bool:
q = logs[key]
while q and q[0] <= t - WINDOW:
q.popleft()
if len(q) < LIMIT:
q.append(t)
return True
return False
# 5 requests at second 58 and 5 more at second 61: 10 requests within 3 seconds.
times = [58] * 5 + [61] * 5
print("fixed window :", sum(fixed_window("k1", t) for t in times), "of 10 allowed")
print("sliding window:", sum(sliding_window("k2", t) for t in times), "of 10 allowed")
# What a good 429 response tells the client:
def response_for(key: str, t: float) -> tuple[int, dict]:
ok = sliding_window(key, t)
remaining = LIMIT - len(logs[key])
headers = {"RateLimit-Limit": str(LIMIT), "RateLimit-Remaining": str(max(remaining, 0))}
if not ok:
headers["Retry-After"] = str(int(logs[key][0] + WINDOW - t))
return (200 if ok else 429), headers
for t in [0, 1, 2, 3, 4, 5]:
print(t, *response_for("k3", t))fixed window : 10 of 10 allowed
sliding window: 5 of 10 allowed
0 200 {'RateLimit-Limit': '5', 'RateLimit-Remaining': '4'}
1 200 {'RateLimit-Limit': '5', 'RateLimit-Remaining': '3'}
2 200 {'RateLimit-Limit': '5', 'RateLimit-Remaining': '2'}
3 200 {'RateLimit-Limit': '5', 'RateLimit-Remaining': '1'}
4 200 {'RateLimit-Limit': '5', 'RateLimit-Remaining': '0'}
5 429 {'RateLimit-Limit': '5', 'RateLimit-Remaining': '0', 'Retry-After': '55'}The client is told exactly when to retry (55 seconds, when its oldest request leaves the window). The token-bucket version with per-tenant token budgets is in provider rate limits and multi-tenancy.
A real example
Example
Fair limits for a public AI API
A company opened its summarisation API to partners. In week one, a single partner's retry bug sent 40 requests per second for an hour, starving everyone else. The team added layered limits backed by Redis: 60 requests per minute and 2 concurrent streams per API key, 2 million tokens per day per partner (configurable by plan), a stricter limit on the expensive /summarise-long route, and 429 with Retry-After. They published the limits in their docs and showed remaining quota in response headers; support tickets about "random failures" disappeared because clients could see exactly what was happening.
The easy way
Start here
Use a rate-limiting middleware or your API gateway's built-in limits, keyed by user or API key, backed by Redis, returning 429 with Retry-After. Add a daily token budget per user for LLM endpoints.
When you grow: the production setup
Teams combine edge protection, gateway limits and in-app per-route and per-tenant limits; use atomic Redis scripts or gateway-native limiters; separate request, concurrency and token budgets; offer plan-based quotas; expose limit headers and docs; and monitor 429s, near-limit clients and abuse patterns.
Trade-offs
What you gain
- Protects availability, upstream quotas and budget.
- Fairness between users and tenants.
- Clear feedback lets clients behave well.
What it costs you
- Limits that are too tight frustrate real users.
- Shared counters add a Redis call per request.
- Many layers can be confusing to debug.
When not to use it
- Never run a public or paid API without limits; internal service-to-service calls may use quotas at the gateway instead.
Common mistakes
- Counters in each server's memory.
- Limiting by IP only.
- Request limits without token or cost limits on LLM endpoints.
- Returning 500 or 403 instead of 429 with
Retry-After. - One global limit for cheap and expensive endpoints.
Interview questions
Try answering out loud first, then open the model answer.
Design rate limiting for an LLM API.
Model answer
Authenticate and key limits by API key, user and tenant; enforce requests per minute, concurrent streams and tokens or cost per day; use sliding-window or token-bucket algorithms with counters in Redis via atomic operations; stricter limits for expensive routes; respond with 429, Retry-After and limit headers; layer edge and gateway protection; and monitor 429 rates and near-limit clients.
Compare fixed window, sliding window and token bucket.
Model answer
Fixed window counts per calendar interval: simplest, but allows up to double the limit around boundaries. Sliding window counts over the trailing interval: accurate and smooth, slightly more state. Token bucket refills at a fixed rate up to a capacity: allows controlled bursts while enforcing an average rate, and is common in APIs and gateways.
How do you rate limit across many servers?
Model answer
Keep counters in a shared fast store such as Redis, updated atomically (INCR with EXPIRE, or a Lua script implementing a sliding window or token bucket), or use an API gateway with built-in distributed limiting. Local in-memory limits can add a cheap first layer but can't enforce a global limit alone.
Quiz
Pick an answer to see whether it’s right and why.
0 of 5 answered
Key terms
- Rate limit
- A cap on how many requests or tokens a client may send in a period. Providers return HTTP 429 when you exceed theirs.
- Token bucket
- A rate-limiting algorithm where a bucket refills at a steady rate and each request takes some units out; when it's empty, requests wait or are rejected. Allows short bursts.
- Sliding window (rate limiting)
- Counting requests over the trailing time period, such as the last 60 seconds, which smooths out boundary bursts.
- Fixed window (rate limiting)
- Counting requests per calendar interval, such as per minute. Simple, but allows bursts at the edges between windows.
- Retry-After
- An HTTP response header telling the client how many seconds to wait before trying again, sent with 429 and 503 responses.
Before and after
Progress is saved in this browser only.