Rate limiting & quotas¶
At a glance
Priority: P0 · Complexity: ⅗ · Est. time: 3 h · Phase: 4 · Prereqs: API design, Caching You're done when: you can compare token bucket, leaky bucket, fixed/sliding window algorithms, design a distributed limiter (Redis, local + global), distinguish rate limits, quotas, concurrency limits and load shedding, and design per-token, per-tenant limits for an LLM gateway.
Why it matters¶
Rate limiting protects availability (against abuse and accidents), enforces fairness across tenants, and implements commercial plans. It is also a frequent interview topic where candidates recite algorithms but miss the real design questions: what is the key (IP, user, API key, tenant, model), where does it run, what happens when the limiter is down, how do clients learn their limits, and what is limited (requests, cost, concurrency).
In 2026 the dominant new case is LLM APIs, where the scarce resource is tokens (and GPU capacity), the cost of a request isn't known until it finishes, and providers limit by RPM, TPM (input and output tokens per minute) and concurrent requests. Anyone building an LLM gateway or agent platform builds a per-token, per-tenant, multi-provider limiter.
Core concepts¶
Four different controls (don't conflate)¶
| Control | Question it answers | Typical mechanism | Response |
|---|---|---|---|
| Rate limit | How fast may a client call? | Token bucket / windows | 429 + Retry-After |
| Quota | How much in a billing period? | Counters per day/month, metering | 429/402 or throttle; upsell |
| Concurrency limit | How many in-flight at once? | Semaphore / adaptive (AIMD, Vegas) | 429/503 or queue |
| Load shedding | Protect the server when overloaded regardless of who | Priority-based drop, adaptive limits | 503 (drop lowest priority first) |
Rate limits protect fairness; load shedding protects the service; conflating them means a well-behaved client gets shed while an abusive one is within its limit. Amazon's Builders' Library on load shedding and Google SRE's overload chapter cover the latter.
Algorithms¶
| Algorithm | State per key | Behaviour | Pros | Cons |
|---|---|---|---|---|
| Fixed window counter | count + window start | Reset at boundaries | Trivial, cheap | Boundary burst: 2× limit across a window edge |
| Sliding window log | timestamp per request | Exact | Precise | O(requests) memory |
| Sliding window counter | 2 counters (current, previous) | Weighted estimate | Cheap and smooth; Cloudflare reports ~0.003% error | Approximate |
| Token bucket | tokens + last refill time | Allows bursts up to capacity, sustained rate = refill | Bursty-friendly, O(1), industry default (Stripe, AWS) | Two params to tune |
| Leaky bucket (as meter/queue) | level + last time | Smooths output to constant rate | Protects downstream with steady flow | Queueing adds latency; bursts dropped |
| GCRA | one timestamp (theoretical arrival time) | Equivalent to token bucket, very compact | Single value; atomic-friendly | Less familiar |
| Adaptive concurrency | in-flight + latency | Adjusts limit from observed latency | No manual tuning; handles capacity shifts | Needs care to avoid oscillation |
Token bucket in Redis (atomic via Lua; store tokens, ts; compute refill on read; set TTL) is the workhorse. Stripe operates request rate limiters, concurrent-request limiters, fleet-usage load shedders and worker-utilisation shedders in layers ("Scaling your API with rate limiters").
Architecture¶
flowchart LR
C[Client] --> E[Edge / WAF<br/>IP-level, DDoS]
E --> G[API gateway<br/>per-key / per-tenant limits]
G --> S[Service<br/>concurrency + load shedding]
G <-- INCR / Lua --> R[(Redis / rate-limit service)]
G -. local token cache .-> G
Where to enforce:
- Edge/CDN/WAF: IP-based, bot/DDoS, coarse; cheap and early.
- Gateway/sidecar: per-API-key/tenant/plan. Envoy supports local rate limiting (per instance, no dependency) and a global rate limit service (gRPC to a shared Redis-backed service).
- In-service: concurrency and priority-aware shedding, which needs application knowledge (cost of endpoint).
Distributed design decisions:
- Centralised counter (Redis): accurate, adds ~0.3–1 ms and a dependency; Redis Cluster shards by key. Hot keys (one giant tenant at 50k RPS) melt a shard → use local pre-aggregation.
- Local approximate + periodic sync: each node enforces
limit / Nor reserves batches of tokens from the central store (Figma's alternative approach; Cloudflare's eventual approach). Cheaper and resilient, at the price of accuracy; fine when limits are "protective", not "billing exact". - Fail-open vs fail-closed: if the limiter store is down — fail open for protective limits (availability over precision) with local fallback limits; fail closed only for security-sensitive limits (login attempts, OTP). Make this an explicit, tested decision.
- Clock/time: derive time from the store (Redis
TIME) or use monotonic elapsed logic to avoid client clock skew. - Race conditions: read-modify-write must be atomic (Lua/
INCR+EXPIREpitfalls: set TTL atomically).
Communicating limits¶
- Return
429 Too Many RequestswithRetry-After; use the IETFRateLimit/RateLimit-Policyheaders (draft) so clients can self-throttle. Provide separate signals for quotas. - Client-side: honour
Retry-After, add jitter, use retry budgets, and prefer client-side token buckets so you don't hammer the server (see Reliability patterns). - Document limits per plan; expose usage APIs/dashboards.
Choosing the key and the fairness model¶
- Keys: API key/tenant (default), user, IP (weak: NAT, IPv6, botnets), endpoint/cost class, model (LLM), and combinations (tenant × endpoint).
- Weighted cost: a
searchmay cost 5 units, aGET1 (GraphQL cost analysis; LLM tokens). - Fair sharing: per-tenant limits plus a global cap; when global capacity is scarce use weighted fair queuing or max-min fairness so one tenant can't starve others. Priority tiers: interactive > batch; batch can use leftover capacity and be preempted.
- Bursts: token bucket capacity = burst tolerance; sustained rate = refill.
LLM rate limiting (2026)¶
Provider limits (Anthropic, OpenAI, Azure OpenAI/Foundry, Bedrock) typically include RPM, input TPM, output TPM (or combined TPM), and concurrency; Azure adds per-deployment quota in TPM allocated per region/subscription. Implications for your own gateway:
- Limit by tokens, not requests. Estimate at admission:
input_tokens (tokenise) + max_tokensreserved; after completion, reconcile with actual usage (refund unused). This "reserve then settle" pattern is the LLM equivalent of a two-phase counter. - Multiple budgets simultaneously: per-tenant TPM, per-tenant monthly $ budget, per-user, per-model, plus the global provider TPM you must not exceed.
- Concurrency limits matter as much as TPM because streaming responses hold capacity for tens of seconds (Little's Law, see Scalability fundamentals).
- Priority lanes: interactive chat vs batch evals; reserve capacity for interactive, let batch scavenge.
- Handle provider 429s: honour
retry-after, fail over to another deployment/region/provider or smaller model, queue with deadlines. Track remaining-limit headers from responses to steer routing proactively. - Agent loops: cap steps, tool calls and cumulative tokens per run (a runaway agent is a self-inflicted DoS and budget burn); per-run budgets are rate limits with a lifetime of one task.
- Abuse and denial-of-wallet: unauthenticated or lightly authenticated endpoints backed by LLMs need strict per-IP/per-user token caps and CAPTCHA/attestation, since a single attacker can burn thousands of dollars.
See Design a multi-tenant LLM gateway and Model routing & gateways.
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| Stripe — Scaling your API with rate limiters | article | Four layered limiters (rate, concurrency, fleet load shedding, worker load shedding) from production | intermediate | free |
| Cloudflare — Counting things, a lot of different things | article | Sliding window counter and distributed counting at massive scale | advanced | free |
| Brandur — Rate limiting with Redis (GCRA) |
article | GCRA explained with a Redis implementation; compact and correct | advanced | free |
| Figma — An alternative approach to rate limiting |
article | Fixed-window counters with sliding approximation and practical trade-offs | intermediate | free |
| Envoy — Global rate limiting | docs | Local vs global limiting architecture in a widely used proxy | advanced | free |
| IETF RateLimit header fields draft | docs | The emerging standard for advertising limits | intermediate | free |
| Anthropic — Rate limits | docs | Real LLM provider limits: RPM, ITPM, OTPM, token-bucket behaviour, cache-aware counting | intermediate | free |
| Azure OpenAI / Foundry quotas and limits | docs | TPM quota per deployment/region — what you plan capacity against | intermediate | free |
| AWS Builders' Library — Using load shedding to avoid overload | article | Load shedding distinct from rate limiting; priorities and goodput | advanced | free |
Hands-on lab¶
Goal: implement token bucket in Redis and an LLM token-aware limiter with reserve/settle (90 min).
- Implement token bucket as a Redis Lua script (
KEYS[1]bucket, args: capacity, refill/s, cost, now fromredis.call('TIME')). Return allowed + remaining + retry_after. Test with 50 concurrent clients viaasyncio. - Compare fixed window vs sliding window counter vs token bucket by sending a burst straddling a window boundary; plot accepted requests over time.
- Build a FastAPI dependency
limit(tenant, tokens_estimate): reserveestimated_input + max_tokens, call the model (Ollama/provider), read actual usage, settle by refunding the difference. Add a per-tenant concurrency semaphore and a global TPM bucket. - Failure test: stop Redis; verify fail-open with local fallback limits for chat, fail-closed for a
/loginroute. - Expected output: fixed window admits ~2× at boundary, sliding/token bucket don't; refunds keep effective throughput near the true token budget; the Redis outage behaves per the documented policy.
Questions¶
L1 — Recall¶
Q1. Compare token bucket and leaky bucket.
Answer
Token bucket: tokens accrue at a fixed rate up to a capacity; each request consumes tokens; it permits bursts up to capacity while enforcing the average rate. Leaky bucket (queue variant): requests enter a queue drained at a constant rate, smoothing output and overflowing when full; (meter variant) equals a token bucket without burst credit. Use token bucket for API limits with burst tolerance; leaky bucket to protect a downstream needing steady flow.
Q2. What is the fixed-window boundary problem?
Answer
A client can send the full limit at the end of one window and the full limit again at the start of the next, achieving up to 2× the intended rate within a short interval spanning the boundary. Sliding window (log or counter) and token bucket avoid it.
Q3. What's the difference between a rate limit and load shedding?
Answer
Rate limits enforce per-client fairness/policy irrespective of server state. Load shedding drops or degrades work when the server is overloaded, prioritising important traffic, irrespective of whether any client exceeded its limit. You need both: the first prevents abuse, the second preserves goodput during genuine capacity shortfalls.
Q4. Why should LLM rate limiting account for output tokens and concurrency, not just request count?
Answer
Cost and capacity scale with tokens (prefill compute and decode time), so RPM says little; providers meter input/output TPM separately. Streaming responses occupy capacity for many seconds, so concurrent stream count (Little's Law) is a binding limit. Output length is unknown at admission, hence reservation with max_tokens and post-hoc settlement.
L2 — Apply¶
Q5. Design a per-tenant limiter for 10k tenants, 200k total RPS, limits from 10 to 5,000 RPS, with a Redis backend.
Answer
Token bucket per (tenant, plan) in Redis with Lua for atomicity; keys sharded by tenant across the cluster (200k RPS of ~1 ms Lua is feasible across a few shards, but reduce load): use local batch leasing — each gateway node holds a small local bucket refilled from Redis in chunks (e.g. every 100 ms or after N tokens), so Redis sees ~10 ops/s per node per active tenant instead of per-request. Heavy tenants (5k RPS) get larger chunks. Accept slight over-admission bounded by outstanding leases. Emit metrics per tenant, expose headers, fail open to local-only limits at 2× conservative rates if Redis is unreachable.
Q6. A client retries immediately on 429 and creates a retry storm. What server and client changes fix it?
Answer
Server: return Retry-After (with jitter server-side to desynchronise), stable rate-limit headers, and consider penalising abusive retriers (increased cost for rejected requests, escalating backoff). Client: exponential backoff with full jitter honouring Retry-After, retry budgets (at most ~10% extra traffic), client-side token bucket to self-throttle, circuit breaker after repeated 429s, and idempotent retries. Document all of this in the SDK defaults, not just in prose.
Q7. You must enforce 1M tokens/minute per tenant against an LLM gateway. Explain the reserve/settle flow with numbers.
Answer
Request has ~3,000 input tokens (counted with the tokenizer) and max_tokens=1,000. Reserve 4,000 from the tenant's TPM bucket (reject/queue with Retry-After if unavailable). Also reserve one concurrency slot. After the response, actual usage is 3,000 + 420 = 3,420; refund 580. If the request errors early, refund everything not consumed. Cached input tokens may count differently by provider; mirror the provider's accounting or you'll drift. Track reservations with TTL so crashed requests don't leak tokens.
L3 — Design & trade-offs¶
Q8. Centralised Redis counters vs local approximate limiters — decide for a public API at 500k RPS.
Answer
Centralised exact counting at 500k RPS costs a Redis round trip on every request (latency, hot keys, and a critical dependency). Local limiters with periodic synchronisation (or token leasing) scale linearly, keep working during Redis outages, and add no per-request latency; error is bounded by sync interval × node count. Choose hybrid: local token buckets with leased budgets for the fast path; exact central accounting for billing quotas (asynchronously aggregated). Use central strictness only where exactness is contractual (per-minute paid SLA caps) or security-critical (login attempts).
Q9. Design fair sharing between interactive chat traffic and batch evaluation jobs on a shared LLM capacity of 2M TPM.
Answer
Two priority classes with reserved and burstable shares: interactive gets a guaranteed 60% (1.2M) plus can borrow unused batch capacity; batch gets 20% guaranteed and scavenges idle capacity, preemptible with queueing and deadlines. Implement with hierarchical token buckets (parent global 2M, children per class, then per tenant), queue batch requests with slow drain and jittered retries, and use provider batch APIs where possible. Admission for interactive uses tight deadlines and fails fast to a fallback model; monitor p95 queue time per class and adjust shares.
Q10. Should the limiter fail open or closed? Justify by endpoint type.
Answer
Protective limits on general APIs: fail open with conservative local fallback limits — an outage of the limiter shouldn't become an outage of the product. Security-critical (login, password reset, OTP, payment attempts): fail closed or use strict local limits, since abuse during a limiter outage is the very scenario attackers exploit. LLM endpoints with cost exposure: fail to tight local per-tenant caps (not fully open) to avoid denial-of-wallet. Test each mode in game days.
L4 — Staff-level ambiguity¶
Q11. A major customer demands 'guaranteed 10k RPS' while your shared platform can't reserve that capacity today. How do you resolve this?
Answer
Separate the commercial promise from the mechanism. Clarify what they need (peak RPS? latency at that rate? burst duration?). Options: dedicated capacity/cell for that customer (isolation, higher price), reserved capacity within the pool via weighted fair share with priority and admission control (guarantee = floor, not exclusive), or a burst-credit token bucket (sustained 3k, bursts to 10k for 60 s). Price accordingly, define SLOs and what happens beyond the guarantee, and include load-test evidence. Escalate capacity planning and align with sales on standard tiers so you don't create a bespoke SLA per customer.
Q12. Leadership wants to cut LLM spend 40% without hurting user experience. How does rate limiting/quotas fit in a broader plan?
Answer
Limits are a governance tool, not the first lever. Sequence: (1) Attribution: cost per tenant/feature/route in the gateway; find the top 5 spenders and unbounded agent loops. (2) Efficiency: prompt caching, routing simple tasks to smaller models, trimming context, batch APIs for offline work. (3) Guardrails: per-run token/step budgets for agents, per-user daily caps for free tiers, per-tenant monthly budgets with alerts at 80%/100%, denial-of-wallet protections. (4) Policy: tiered plans where heavy use is priced. Communicate effect sizes and monitor quality metrics (eval scores, user satisfaction) to ensure savings don't regress experience. Present the plan with expected savings per lever.
Real-world use cases¶
- Stripe: layered request, concurrency and load-shedding limiters protect the API.
- Cloudflare: distributed counting with sliding windows at the edge.
- GitHub/Slack: per-token rate limits with headers; GraphQL cost-based limits.
- LLM gateways (LiteLLM etc.): per-key/team TPM/RPM, budgets, and fallback on provider 429s.
- Logistics partner APIs: per-partner quotas and bursts around vessel-schedule publication times.
Pitfalls & anti-patterns¶
- Limiting only by IP.
- Non-atomic read-modify-write in the limiter (races).
- Fail-open without fallback limits; fail-closed for everything.
- 429 without
Retry-After; clients retrying instantly. - Limiting LLM requests by RPM only; no per-run agent budgets.
- Using rate limits to solve genuine capacity shortfalls (that is load shedding and capacity planning).
Checklist¶
- I can compare token bucket, sliding window, GCRA and leaky bucket
- I built a Redis token bucket with Lua and tested failure modes
- I can design reserve/settle token limiting for LLMs
- I can separate rate limits, quotas, concurrency limits and load shedding
- I answered all L3 questions out loud in < 3 min each