Handbooks / System Design Case Studies / Chapter 2
Design a Rate Limiter
13 pages · ~20 min✓ Reviewed
Builds on Design a URL Shortener. Next up: Design a Distributed Key-Value Store.
Problem, Scope and Requirements (API Rate Limiter)
1. The Problem
- Cap requests per client per time window
- Protects backends from abuse, overload and noisy neighbours
- Decision per request: allow or reject, before the backend is touched
| In scope | Out of scope |
|---|---|
| Per-client request limits | DDoS scrubbing at L3/L4 |
| Allow/reject decision, 429 reply | Billing and usage invoicing |
| Live rule changes | Network-level traffic filtering |
2. Functional Requirements
- Configurable rules: client, endpoint, limit, window
- Return 429 with
Retry-Afterwhen rejected - Rule changes go live without redeploy
- Example tiers: free 100/min per API key, paid 1,000/min, login 5/min per IP
free: key=api_key limit=100 window=60s paid: key=api_key limit=1000 window=60s login: key=ip limit=5 window=60s endpoint=/login
one rule = client key + endpoint + limit + window
- RequestAPI key / IP
- Match ruleclient + endpoint
- Check countlimit per window
- Allow or 429Retry-After
High-Level Architecture (API Rate Limiter)
1. Request Path
- Clientsends request
- Load balancerspreads over 50 gateways
- Gatewaylimiter middleware
- Redis clustercounter check
- Backend or 429allow or reject
- Enforce at the edge: rejected traffic never reaches the backend
- Local in-process check first (cheap, no network)
- Redis check second, only when the local check passes
- Reject answers 429 straight from the gateway
2. Components
Token Bucket and Leaky Bucket (API Rate Limiter)
1. Token Bucket
- Bucket holds up to B tokens (capacity), refilled at R tokens/s
- Each request takes 1 token; empty bucket means reject with 429
- Allows bursts up to B, then settles to R req/s
- Requestkey = client
- Lazy refillmin(B, tokens + elapsed × R)
- tokens ≥ 1?take 1 token
- Allow or 429empty = reject
2. Lazy Refill and Example
Fixed and Sliding Window Algorithms (API Rate Limiter)
1. Fixed Window and Its Flaw
- Key =
client:floor(now/window), so every request in one window hits the same counter - Per request:
INCRthe key, andEXPIREit on the first hit - O(1) memory: one integer per client, the simplest algorithm
- Reject once the count passes the limit, e.g. 100/min
window = floor(now / 60) key = "u42:" + window count = INCR key if count == 1: EXPIRE key 120 allow = count <= 100
limit 100/min, TTL 2 × window
- 112:00:59100 requests pass
- 212:01:00new key, counter back to 0
- 312:01:00100 more requests pass
- 4200 in 2 s2× the limit
Redis-Based Counters and Data Model (API Rate Limiter)
1. Key Schema
- Schema:
rl:{client_id}:{rule_id}:{window_start} - Wrap only client_id in braces: it is the hash tag
- Redis Cluster hashes the first
{...}pair only - So all of one client's keys share a slot and one shard
| Algorithm | Key (client u42) | Type | Holds |
|---|
Race Conditions and Atomicity (API Rate Limiter)
1. The Check-Then-Set Race
- Limit 100/min is checked by 50 gateways at the same time, so reads and writes interleave
- A GET then SET is two round trips: nothing stops another node from slipping in between
- Result: every lost update lets one extra request through; under load the overshoot grows
- 1GW-A: GETreads 99
- 2GW-B: GETreads 99
- 3Both allow99 < 100
- 4Both SET 100last write wins
- 5101 served1 extra slipped through
2. Atomic Fixes: INCR and Lua
Limit Keys and Rules: User, IP, API Key (API Rate Limiter)
1. Pick the Limit Key
- Order: API key → user ID → IP
- IP is only the fallback for anonymous traffic
- Rule = (key, endpoint, limit, window), looked up per request
2. Why IP Alone Fails
Response Headers and Client Behaviour (API Rate Limiter)
1. Rejecting and Headers
- Reject with 429 Too Many Requests
- Never 503 (looks like an outage) and never 403 (looks like a permission failure)
- Send the headers on every response, so clients can slow down before they hit 429
| Header | Meaning | Unit |
|---|---|---|
X-RateLimit-Limit | Allowed requests in the window |
Distributed Deployment and Synchronization (API Rate Limiter)
1. Centralized Redis
- Exact counts: every gateway updates one shared counter
- Cost: one network hop, about 0.5 ms per request
- Redis becomes a hot dependency: 1M req/s peak all lands on it
- Fits the budget: under 2 ms p99 added latency
2. Local Counters and Token Leases
- Local counters sync with Redis every 100 ms: no hop per request
- Overshoot is up to nodes × local batch size
Failure Modes and Metrics (API Rate Limiter)
1. When Redis Is Down
| Fail-open | Fail-closed | |
|---|---|---|
| Action | Allow all requests | Reject requests |
| Protects | Availability | Security |
| Use for | Default for most APIs | Login, payments |
| Risk | Abuse while blind |
Scaling, Trade-offs and Interview Questions (API Rate Limiter)
1. Scale Path to 1M req/s
- Shard8 masters
- Replicate1 replica each
- Cache locally-80% Redis calls
- Shard: 1M req/s ÷ 150k ops/s ≈ 7, so 8 masters sized for the uncached peak
- Only the 8 masters hold counters: about 250 MB each (2 GB total)
- Replicate: 1 replica per master for failover; replicas hold copies, not extra capacity
- Local cache: 80% fewer Redis calls leaves about 200k ops/s
- After the cache, 2 shards could carry the load; the other 6 are headroom for spikes and failover
Rate Limiter Cheatsheet (API Rate Limiter)
1. Algorithms to Remember
- Token bucket: capacity B, refill rate R tokens/s, 1 token per request
- Lazy refill:
tokens = min(B, tokens + (now - last) × R), no timer - State is 2 numbers per key; burst friendly up to B
- Example: B=10, R=5/s lets 10 pass instantly, then 5 req/s
- Time comes from the server or Redis TIME, never the client
- Sliding counter:
est = cur + prev × (1 - fraction)
Check yourself
A fixed-window limiter allows 100 requests per minute. A client sends 100 requests at 12:00:59 and 100 more at 12:01:00. How many are allowed, and does a sliding window counter allow the second batch?
- Fixed window: all 200 pass, because the two batches fall in different window keys.
- That is 200 requests in about 1 second, double the intended rate (the boundary burst).
- Sliding counter at 12:01:00: elapsed fraction is 0, so the estimate is cur + prev × 1 = 0 + 100 = 100.
- That already hits the limit, so the second batch is rejected.