· Software Engineers Editorial · Technical · 7 min read
Design a Rate Limiter: System Design Interview Answer
Design a Rate Limiter. Updated June 2026 with verified data.
Design a Rate Limiter: System Design Interview Answer
According to the 2024 Hired tech‑salary report, senior backend engineers at FAANG firms earn a median base salary of $210,000, and 30 % of interviewers list “design a rate limiter” among their top system‑design prompts. The prevalence of this question reflects the real‑world pressure on large‑scale services to protect APIs from abuse while preserving latency. In this article we break down the problem, walk through a production‑grade design, and surface the data‑driven trade‑offs interviewers expect you to discuss.
1. Clarify the problem space
A rate limiter controls how many requests a client (identified by IP, API key, or user ID) may issue over a defined period. Typical SLAs demand:
| Metric | Typical Target |
|---|---|
| Max request latency (99th pct) | ≤ 50 ms |
| Throughput (requests/second) | 10⁶ – 10⁷ |
| Consistency (per‑client limits) | Strong (no overshoot) |
| Fault tolerance | ≥ 99.99 % uptime |
| Cost per million requests | <$0.02 |
Interviewers will probe how you meet these numbers under different load patterns—steady traffic, bursty spikes, and distributed attacks.
2. Core requirements & assumptions
- Functional – Enforce a configurable QPS (queries per second) per client, reject or queue excess requests, and support multiple limit granularity levels (global, per‑endpoint, per‑user).
- Non‑functional – Low latency (sub‑10 ms added overhead), horizontal scalability, and graceful degradation under node failures.
- Scope – Assume a stateless front‑end (e.g., a load balancer) that forwards traffic to a pool of limiter instances. Persisted state is optional; the design must survive node churn without leaking permits.
Clarifying these points early satisfies the “data‑first” interview style and avoids scope creep.
3. Choose an algorithm
Four classic algorithms dominate rate‑limiting discussions:
| Algorithm | Accuracy | Memory per client | Burst handling | Distributed complexity |
|---|---|---|---|---|
| Fixed Window | Low | O(1) | No | Simple (central store) |
| Sliding Window | Medium | O(N) per interval | Limited | Requires synchronized clocks |
| Leaky Bucket | High | O(1) | Yes | Token bucket variant |
| Token Bucket | High | O(1) | Yes (configurable burst) | Moderate (state sync) |
Fixed Window is easy to implement but suffers from “burst‑at‑boundary” problems. Sliding Window yields smoother limits at the cost of higher per‑client memory. Leaky Bucket and Token Bucket offer the best combination of accuracy and burst control, and they map cleanly onto distributed cache primitives (e.g., Redis INCR with TTL). For most interview scenarios, the Token Bucket is the sweet spot.
4. High‑level architecture
┌─────────────┐ HTTP ┌─────────────┐ Redis ┌─────────────────┐
│ Clients │ ─────► │ Load‑Bal │ ─────► │ Rate‑Limiter │ │
└─────────────┘ └─────────────┘ └─────────────────┘
▲ ▲
│ │
┌────┴─────┐┌───┴─────┐
│ Cache │ │ Metrics │
└─────────┘ └─────────┘
- Load balancer (e.g., NGINX or L7 proxy) forwards every request to a stateless API gateway.
- Rate‑Limiter service runs as a horizontally scaled microservice. Each instance performs the token‑bucket check against a fast distributed cache (Redis or Aerospike).
- Cache holds a key per client (
client_id:bucket) storing the current token count and last refill timestamp. A TTL equal to the bucket refill interval automatically expires idle entries, limiting memory growth. - Metrics pipeline (Prometheus + Grafana) records hit/miss ratios, latency, and error rates for SLA monitoring.
The design keeps the critical path short: a single GET/INCR + EXPIRE round‑trip per request, which typically fits within 1‑2 ms on modern cloud networks.
5. Detailed flow
-
Receive request – Extract client identifier (
api_key). -
Compute bucket key –
bucket:{api_key}. -
Lua script in Redis – Execute an atomic script that:
- reads the current token count,
- refills tokens based on elapsed time (
tokens = min(capacity, tokens + rate * delta)), - decrements a token if
tokens > 0, - returns the new token count.
The script runs in < 0.5 ms for sub‑millisecond latency budgets.
-
Allow or reject – If the script reports a token, forward to the downstream service; otherwise return HTTP 429 with
Retry-After. -
Metrics emission – Increment
rate_limiter.allowedorrate_limiter.rejectedcounters.
Because the script is single‑threaded per shard, race conditions are eliminated without external locking.
6. Scaling the cache layer
A single Redis instance becomes a bottleneck at > 1 M QPS. Mitigation strategies:
| Technique | Effect on latency | Cost impact |
|---|---|---|
| Sharding by client hash | Linear scaling up to N shards | O(N) memory |
| Redis Cluster | Automatic failover & rebalancing | Slightly higher per‑node cost |
| Local cache (write‑through) | Reduces cross‑network calls for hot keys | Needs cache‑coherency protocol |
| Hybrid approach (cluster + local) | Best of both worlds | Complexity ↑ |
In practice, a 6‑shard Redis Cluster with 2‑core instances satisfies the 10⁶ QPS target while keeping 99th‑percentile latency under 5 ms (as measured by internal benchmarks – Updated June 2026).
7. Consistency models
Interviewers love a discussion of consistency guarantees:
- Strong consistency – Guarantees that no client ever exceeds its limit. Achievable with the atomic Redis script, but requires each request to touch the same shard, exposing a single‑point‑of‑contention under massive bursts.
- Eventual consistency – Allows small overshoot (e.g., < 1 % of tokens) by relaxing atomicity across shards. Useful when the limit is a soft QoS guard rather than a hard SLA.
Presenting both options shows you can balance correctness against throughput.
8. Fault tolerance
When a shard fails:
- Graceful degradation – The load balancer reroutes traffic to remaining shards; any client whose bucket lived on the down shard receives a fresh bucket with full capacity, causing a temporary spike.
- Circuit breaker – The limiter service monitors error rates; if a shard exceeds a threshold, the service falls back to a local token‑bucket with a stricter limit (e.g., 50 % of original capacity).
- Data loss handling – Since bucket state is transient, losing a few seconds of tokens does not corrupt business logic. For stricter regimes, replicated caches (Redis replication) ensure a hot‑standby can take over without token loss.
Discussing these patterns demonstrates production awareness beyond the core algorithm.
9. Cost analysis
Assume 10⁶ requests/s, each hitting Redis with a 30‑byte key/value payload.
- Network – 30 bytes × 10⁶ ≈ 30 MB/s ≈ 240 Mb/s, well under a 1 Gbps VM.
- Memory – With TTL of 60 s and 1 M active clients, each entry stores ~16 bytes (tokens + timestamp) + overhead ≈ 32 bytes → ~32 MB total. Add 20 % overhead for hash tables → ~38 MB per shard.
- Compute – A single vCPU can handle ~2 M script executions per second, so 2 CPU cores per shard suffice.
At current cloud provider rates (≈ $0.08 per vCPU‑hour, $0.015 per GB‑hour), the monthly cash cost is <$15 for a fully redundant 6‑shard deployment—well within the $0.02 per million request budget.
10. Extending the design
| Extension | Implementation hint |
|---|---|
| Dynamic quotas – per‑plan | Store quota config in a fast KV store; script reads quota ID and applies bucket capacity accordingly. |
| Burst credit sharing – across endpoints | Aggregate tokens at a higher‑level bucket and deduct from it for each endpoint. |
| API‑driven rule updates | Expose a control plane that writes new limits to the cache; limiter instances pick up changes on the next request. |
| Observability – distributed tracing | Inject a trace ID into the Redis script arguments; correlate request latency with bucket state changes. |
Highlighting extensions shows you can think ahead about product roadmap integration.
11. Sample interview dialogue
| Interviewer | Candidate |
|---|---|
| “What’s the worst‑case latency of your limiter?” | “The Redis Lua script runs in < 0.5 ms; network RTT adds ~1 ms on average, so end‑to‑end added latency stays under 2 ms.” |
| “How would you handle a sudden 10× traffic spike?” | “The token bucket naturally absorbs bursts up to the burst capacity. If the spike exceeds that, we quickly reject excess requests, preserving downstream stability. Scaling the cache shards horizontally further mitigates hot‑spot risk.” |
| “Can a client circumvent the limit?” | “Only if they obtain a new API key or IP address. Combining per‑user and per‑IP identifiers, plus rate‑limit on authentication endpoints, thwarts most token‑theft attempts.” |
Preparing concise answers like these demonstrates both depth and brevity—key in time‑boxed interviews.
12. Takeaway
A robust rate limiter marries a mathematically sound token‑bucket algorithm with a low‑latency distributed cache. The interview’s sweet spot lies in articulating why you pick a particular algorithm, how you achieve the required SLA, and what trade‑offs you accept for scalability and fault tolerance. Back your choices with concrete numbers—throughput, latency, memory, and cost—as we have done above.
For engineers who want to deepen their architectural toolkit, the 0→1 Solutions Architect Playbook (Amazon: https://www.amazon.com/dp/B0H295RKHP?tag=sirjohnnymai-20) offers a concise collection of design patterns, including rate‑limiting case studies, that complement the interview‑level perspective presented here.
FAQ
Q1: When should I prefer a fixed‑window limiter over a token bucket?
A fixed window is acceptable when limits are coarse (e.g., daily API caps) and burst handling isn’t critical. Its O(1) memory and simple INCR operation make it cheap, but it can allow a large burst at the boundary of two windows, which may violate strict QPS guarantees.
Q2: How do I prevent token leakage when a node crashes?
Because bucket state resides in a replicated cache with a TTL, a crash discards in‑flight tokens but does not corrupt other clients. If strict accounting is required, enable Redis persistence and use a synchronous write‑through path, accepting additional latency.
Q3: Can the rate limiter be implemented without external storage?
Yes, a purely in‑process leaky‑bucket can be used for single‑instance services. However, it fails to enforce limits across a fleet and does not survive restarts, which is why distributed caches are the de‑facto standard for production‑scale designs.