Rate Limiter
Design a distributed rate limiter that caps how many requests a client can make in a given time window, returning 429 when exceeded.
These designs are for learning purposes and may not be 100% accurate or production-ready. Always cross-reference with official docs and other resources. Report an issue if you spot something wrong.
Reference Documents
Scaling Your API with Rate Limiters — Stripe Blog
Stripe Engineering's post on four rate-limiting strategies (request rate, concurrency, fleet utilization, worker utilization) used in production payment infrastructure.
BlogRate Limiting Fundamentals — ByteByteGo
Deep dive into token bucket, sliding window, and leaky bucket algorithms for rate limiting.
BlogCounting Things: A Lot of Different Things — Cloudflare Blog
Cloudflare's engineering post on how they implement distributed rate limiting at massive scale.
ArticleRate Limiter System Design — YouTube
Video walkthroughs of rate limiter design, covering algorithms and distributed approaches.
VideoOverview
What is a Rate Limiter?
A rate limiter caps how many requests a client (by IP, user ID, or API key) can make in a given time window. Requests over the limit receive 429 Too Many Requests.
Key operations:
- Check: Is this client under their limit?
- Increment: Record this request against their quota
- Block: Reject if over limit, pass through if under
Requirements
Functional
- Limit requests per client per time window (e.g. 100 req/min)
- Support multiple limit keys: IP, user ID, API key
- Return
Retry-Afterheader on 429 - Allow different limits per endpoint/tier
Non-Functional
- Latency: < 5ms overhead per request
- Highly available — must not block all traffic if limiter fails
- Distributed: works across multiple app servers
Capacity
- 100K req/sec × 1 Redis INCR = 100K ops/sec
- Redis handles 1M+ ops/sec — well within limits
Design Intuition
Core Problem: Distributed Counter
Local counters don't work — 10 servers with limit=100 means a user could make 1,000 requests. You need a shared counter across all servers.
Algorithm Comparison
| Algorithm | Pros | Cons |
|---|---|---|
| Fixed Window | Simple, O(1) | 2× burst at boundary |
| Sliding Window Log | Exact | High memory |
| Sliding Window Counter | Accurate + low memory | Slight approximation |
| Token Bucket | Smooth bursts | Complex |
Sliding window counter with Redis is the best balance.
Redis atomicity
Redis Lua scripts make increment + check race-condition-free. Single Redis node handles 1M+ ops/sec.
Fail open vs fail closed
If Redis is down: fail open (let traffic through) for availability, fail closed (block all) for security. Most APIs choose fail open with alerting.
High-Level Design
Request Flow
- Request hits API Gateway
- Gateway calls Rate Limiter with (client_id, endpoint, timestamp)
- Rate Limiter runs Redis Lua script — increment counter, check against limit
- Over limit → 429 +
Retry-Afterheader - Under limit → forward to App Server
Redis Key Schema
rate:{client_id}:{endpoint}:{window_unix} → integer
TTL = window_size_seconds
Rules DB
Stores per-client/per-tier limits. Cached in-memory in the Rate Limiter (refreshed every 60s).
Deep Dive
Sliding Window Counter
Approximates a sliding window using two fixed buckets:
smoothed = prev_count × (1 - elapsed_fraction) + curr_count
If smoothed > limit → reject.
Atomic Redis Lua Script
local count = redis.call('INCR', KEYS[1])
if count == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return count
High Scale
For very high throughput, shard Redis by client_id hash. No cross-shard ops needed.
Trade-offs
| Decision | Option A | Option B | Chosen |
|---|---|---|---|
| Counter store | Redis | Local memory | Redis — shared across servers |
| Algorithm | Token Bucket | Sliding Window Counter | Sliding Window — simpler |
| Failure mode | Fail open | Fail closed | Fail open — availability first |
| Enforcement | Gateway | App layer | Gateway — single point |
Interview Tips
What Interviewers Look For
- Distributed counter — why local counters break at scale
- Algorithm comparison — token bucket vs sliding window trade-offs
- Redis atomicity — Lua scripts prevent race conditions
- Headers —
X-RateLimit-Remaining,Retry-After - Failure mode — fail open vs closed
- Tiers — different limits for free vs paid users
Common Mistakes
- Local in-memory counters only
- Not addressing race conditions
- Forgetting what happens when the limiter itself is down