Rate Limiting Algorithms in System Design Interviews

When an interviewer asks you to "design a distributed rate limiter," they aren't just testing whether you know what an HTTP 429 status code is. They want to see how you evaluate trade-offs between memory efficiency, clock synchronization, burst tolerance, and distributed race conditions.
Rate limiting is a cornerstone question in senior and staff engineering rounds. Whether protecting public APIs against brute-force abuse or shielding downstream database clusters from cascading failures, your choice of rate-limiting algorithm dictates both latency and infrastructure costs.
In this deep dive, we break down core rate-limiting mechanics, compare the four dominant algorithms, analyze a production-ready distributed Redis design, and reveal how to communicate these technical choices during a system design interview.
Why Interviewers Love Rate Limiting Questions
A rate limiter sounds deceptive in its simplicity: track how many requests a user or client makes in a time window, and reject requests that exceed the quota. Yet under real-world scale, simple counters crumble under concurrency issues and multi-region synchronization bottlenecks.
Interviewers use this problem as an architectural prism to assess:
- Algorithmic Foundation: Do you understand the space/time trade-offs between memory-light approximations and mathematically exact windows?
- Distributed Systems Knowledge: How do you avoid race conditions when hundreds of API gateway worker processes access shared state across multiple servers?
- Resilience & Graceful Degradation: What happens when the rate-limiting storage cluster becomes unreachable? Does traffic halt, or does the system fail open?
- Client Communication: How do you construct clear HTTP headers (
X-RateLimit-Limit,X-RateLimit-Remaining,Retry-After) so well-behaved clients throttle themselves?
If you are structuring your roadmap according to our complete guide to technical interview prep, mastering this topic provides patterns applicable across API gateways, microservices caching, and queue throttlers.
Visualizing the Architectural Approaches
Before writing a single line of pseudocode, you must establish an intuitive mental model for how requests navigate quota checks:

The diagram above contrasts three distinct paradigms:
- Token Bucket: Accumulates tokens periodically and supports sudden traffic bursts as long as tokens remain.
- Leaky Bucket: Queues incoming traffic and drains at an uncompromising, fixed rate for smooth downstream processing.
- Distributed Redis Counter: Centralizes quota state across horizontal application nodes using atomic primitives.
Let us dissect each algorithmic strategy in detail.
1. Token Bucket Algorithm
The Token Bucket algorithm is by far the most popular rate-limiting approach in industry, powering production API platforms including Stripe and Amazon Web Services.
How It Works
Imagine a bucket with a predefined capacity $C$.
- A background ticker or timestamp delta adds tokens to the bucket at a constant fill rate $R$ (e.g., 10 tokens per second).
- Tokens stop accumulating once the bucket capacity $C$ is reached (overflowing tokens are discarded).
- When a request arrives, the rate limiter attempts to draw 1 token (or $N$ tokens for weighted API calls).
- If sufficient tokens are present, the token is deducted and the request proceeds immediately.
- If the bucket is empty, the request is dropped with an HTTP 429 status code.
import time
class TokenBucket:
def __init__(self, capacity: int, fill_rate: float):
self.capacity = capacity
self.fill_rate = fill_rate # tokens per second
self.tokens = float(capacity)
self.last_refill = time.time()
def allow_request(self, tokens_needed: int = 1) -> bool:
now = time.time()
elapsed = now - self.last_refill
# Lazy refill calculation avoids running a continuous background thread
self.tokens = min(self.capacity, self.tokens + (elapsed * self.fill_rate))
self.last_refill = now
if self.tokens >= tokens_needed:
self.tokens -= tokens_needed
return True
return False
Key Engineering Trade-offs
- Memory Footprint: Extremely compact ($O(1)$ per key). You only need two numbers per user:
tokensandlast_refill_timestamp. - Burst Handling: Handles bursts seamlessly up to bucket capacity $C$.
- Downstream Protection: Because it tolerates bursts, spikes can still impact downstream databases if $C$ is set too high.
2. Leaky Bucket Algorithm
While Token Bucket meters access while permitting bursts, the Leaky Bucket algorithm acts as a strict smoother for uneven traffic.
How It Works
The bucket is implemented as a First-In, First-Out (FIFO) queue of fixed capacity $Q$.
- When requests arrive, they enter the back of the queue.
- If the queue is full, incoming requests are dropped or rejected immediately.
- At constant intervals (rate $L$), requests are pulled from the front of the queue and processed by downstream services.
In a system design discussion, highlight that Leaky Bucket transforms bursty, unpredictable network traffic into an ultra-predictable, constant outflow stream. This makes it ideal for rate-limiting write operations to transactional databases or asynchronous payment gateways.
Key Engineering Trade-offs
- Memory Footprint: $O(Q)$ per bucket if buffering payloads, or $O(1)$ if tracking request queue count with timestamps.
- Traffic Shaping: Zero bursts downstream. Downstream systems always experience a calm, stable load.
- Latency Penalty: Bursty user requests must sit in the queue until earlier items drain, increasing p99 latency for interactive users.
3. Fixed Window Counter
The simplest algorithm to explain is the Fixed Window Counter. The timeline is segmented into rigid windows (e.g., 60 seconds starting at :00).
How It Works
Each client identifier (such as an IP address or API token) has an integer counter associated with the current time bucket:
- When a request arrives, look up the counter for
user_id:current_minute. - If the counter is less than the limit $N$, increment it and accept the request.
- If counter $\ge N$, reject the request until the clock ticks over to the next minute.
The Critical Edge Case: Boundary Burst
Interviewers routinely probe candidates on the boundary burst flaw:
Suppose the limit is 100 requests per minute. An attacker sends 100 requests at 00:59 (allowed) and another 100 requests at 01:01 (allowed). In that 2-second interval spanning the window boundary, 200 requests entered the system—twice the allowed rate. Mentioning this edge case spontaneously proves you have production architectural experience.
4. Sliding Window Log vs. Sliding Window Counter
To solve the boundary burst issue without massive memory overhead, engineers developed sliding window variants.
Sliding Window Log
Stores the timestamp of every request in a sorted set (like Redis ZSET). When a new request arrives:
- Prune timestamps older than
now - window_sizeusingZREMRANGEBYSCORE. - Count the remaining elements using
ZCARD. - If count #x3C; N$, add the new timestamp with
ZADDand allow the request.
Trade-off: Perfectly accurate, but memory-intensive ($O(N)$ memory per active client). A burst of 10,000 requests consumes 10,000 entries in Redis.
Sliding Window Counter (Weighted Hybrid)
The sliding window counter combines fixed window memory efficiency ($O(1)$) with sliding window accuracy. It estimates requests in the sliding window using a weighted sum:
$\text{Requests in Window} = (\text{Previous Window Count} \times \text{Overlap Ratio}) + \text{Current Window Count}$
If the current window is 30% through and the previous window had 80 requests, the estimated prior contribution is $80 \times 0.70 = 56$. If the current window has 20 requests, the total estimated traffic is $76$. If $76 < \text{Limit}$, allow the request. Cloudflare and Figma rely on this hybrid formula because it achieves 99.9% accuracy with minimal memory.
Algorithm Comparison Matrix
| Algorithm | Memory per Client | Burst Tolerance | Downstream Protection | Implementation Complexity |
|---|---|---|---|---|
| Token Bucket | $O(1)$ (2 variables) | High (up to capacity) | Moderate | Low |
| Leaky Bucket | $O(1)$ or $O(Q)$ | None (smooths traffic) | Maximum (constant outflow) | Medium |
| Fixed Window | $O(1)$ (1 integer) | High at boundary (2x limit) | Poor | Very Low |
| Sliding Log | $O(N)$ (per request) | Exact enforcement | High | Medium |
| Sliding Counter | $O(1)$ (2 integers) | Smooth approximation | High | Medium |
Building a Production-Ready Distributed Rate Limiter
In an interview, once you settle on the algorithm, the interviewer will challenge you on scale: "We have 50 API gateway instances handling 100,000 requests per second. How do you implement this in a distributed cluster?"
Here is how you structure your distributed solution.
1. Centralized High-Performance In-Memory Cache
Local in-memory rate limiting within each API gateway fails because requests from the same user are distributed across multiple instances by the load balancer. You need a fast, centralized store: Redis or Memcached.
Redis is ideal because it supports atomic operations, expiring keys, and embedded Lua scripting.
2. Solving Race Conditions with Lua Scripts
A naive read-then-write approach creates classical race conditions:
Thread A: Read count (99)
Thread B: Read count (99)
Thread A: Count < 100 -> Allow -> Increment to 100
Thread B: Count < 100 -> Allow -> Increment to 101 (Limit Violated!)
To guarantee atomicity without distributed locks (which add latency), execute the entire check-and-decrement logic inside an atomic Redis Lua script:
-- keys[1]: rate limit key (e.g. rate:user123)
-- argv[1]: capacity (e.g. 100)
-- argv[2]: fill_rate (tokens/sec)
-- argv[3]: now (unix epoch timestamp in seconds)
-- argv[4]: tokens_requested (usually 1)
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local fill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local data = redis.call("HMGET", key, "tokens", "last_updated")
local tokens = tonumber(data[1])
local last_updated = tonumber(data[2])
if not tokens then
tokens = capacity
last_updated = now
else
local elapsed = math.max(0, now - last_updated)
tokens = math.min(capacity, tokens + (elapsed * fill_rate))
last_updated = now
end
if tokens >= requested then
tokens = tokens - requested
redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated)
redis.call("EXPIRE", key, math.ceil(capacity / fill_rate) * 2)
return {1, math.floor(tokens)} -- 1 = Allowed
else
redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated)
return {0, math.floor(tokens)} -- 0 = Throttled
end
Because Redis executes Lua scripts sequentially on a single thread per shard, this operation executes in sub-millisecond time with zero concurrency anomalies.
3. Fail-Open vs. Fail-Closed Resilience
What happens when your Redis cluster fails or network partitions occur?
Always address this operational trade-off proactively:
- Fail Closed: Block all requests if the rate limiter cannot verify quotas. Use this for costly third-party AI endpoints or fraud-sensitive payment routes.
- Fail Open: Let traffic pass if Redis times out (e.g. timeout $> 5\text{ms}$). Log an alert and allow requests through to preserve user experience during infrastructure blips.
Framing these choices in terms of architecture decision records in system design shows senior-level architectural thinking that interviewers appreciate.
Communicating Rate Limits via HTTP Headers
A production API must inform clients about their consumption status. Standardize your response headers:
X-RateLimit-Limit: Maximum requests permitted within the current period.X-RateLimit-Remaining: Number of requests remaining in the active window.X-RateLimit-Reset: Unix timestamp when the quota completely replenishes.Retry-After: For 429 responses, the exact number of seconds the client must pause before retrying.
Advising clients to implement exponential backoff with random jitter prevents "thundering herd" spikes when thousands of throttled clients retry at the exact same second.
How to Present This in Your Interview
When tackling this question in a live technical screen, follow this chronological structure:
- Clarify Requirements (3-5 mins):
- What are we rate-limiting (IP, user ID, API key, route)?
- What is the scale (read vs. write requests per second)?
- Is strict accuracy required, or is an approximation acceptable?
- High-Level Diagram (5-8 mins):
- Place the rate limiter at the API gateway tier before downstream microservices.
- Show the shared cache layer.
- Algorithm Deep Dive (10-15 mins):
- Propose Token Bucket as the default for APIs.
- Compare with Leaky Bucket or Sliding Window to show breadth.
- Distributed Bottlenecks (10 mins):
- Discuss Redis single-point-of-failure mitigation with clustering and read replicas.
- Explain race conditions and Lua script atomicity.
- Address multi-region synchronization (local caches with asynchronous batch sync vs. global Redis).
If you feel nervous during live architectural screens, review our strategies for dealing with interview anxiety and prepare thoughtful questions from our list of questions you should ask your interviewer to conclude the session strongly.
Practice Rate Limiting Scenarios with AI
System design interviews require synthesizing algorithms, distributed constraints, and rapid whiteboard communication under real-time pressure. The fastest way to build confidence is by practicing realistic architectural scenarios.
On totop.app, you can simulate interactive system design rounds with AI interviewers tailored to specific job postings and seniority levels. You will receive immediate, granular feedback on your trade-off analysis, concurrency handling, and architectural clarity.
Start practicing today on totop.app and transform your system design prep into guaranteed interview confidence.