Metadata
| Field | Value |
|---|---|
| Slug | rate-limiter |
| Difficulty | Intermediate |
| Estimated Reading Time | 12 min |
| Estimated Coding Time | 20 min |
| Tier | 1 — Core Backend Components |
| Implementation Language | Python (Redis) |
| SEO Description | Master rate limiting algorithms for system design interviews. Learn Token Bucket, Leaky Bucket, Fixed Window, and Sliding Window logs. Includes a Python/Redis implementation. |
1. Overview
What problem does it solve?
No system has infinite resources. If a single user (or a malicious botnet) sends 10,000 requests per second to your API, your servers will crash, database connections will exhaust, and legitimate users will be locked out.
A Rate Limiter controls the rate of traffic sent by a client or a service. If the API allows 10 requests per minute, the 11th request is blocked and returned with an HTTP 429 Too Many Requests status code.
What breaks without it?
- DDoS Attacks: Attackers easily overwhelm your servers.
- Resource Starvation: One poorly coded script by a customer can consume 99% of your server capacity (the "Noisy Neighbor" problem).
- Runaway Costs: If your backend autoscales or calls paid 3rd-party APIs (like OpenAI), an infinite loop could cost you thousands of dollars.
2. Motivation
Rate limiting is usually implemented inside the API Gateway (Block 008) or a Load Balancer (Block 007).
But in a distributed system, rate limiting is hard. If a user is allowed 10 requests per minute, and you have 5 API Gateway servers, how do they coordinate? If Gateway A allows 5 requests and Gateway B allows 5 requests, they need a fast, centralized way to share state. This is why distributed rate limiters almost exclusively rely on fast in-memory stores like Redis.
3. Real-World Usage
| System | Rate Limit Strategy | Use Case |
|---|---|---|
| Stripe | 100 req/sec | Uses Redis + Token Bucket to protect the payments API. |
| 900 req/15 min | Rate limits read endpoints to prevent scraping. | |
| GitHub API | 5,000 req/hour | Per-user rate limiting using Sliding Window. |
| Lyft / Uber | Env-specific | Prevents brute-forcing SMS login codes. |
4. Prerequisites
| Concept | Block |
|---|---|
| API Gateway | 008 API Gateway |
| In-Memory Cache | 010 Caching (Conceptual overlap with Redis) |
5. Visual Explanation
The 4 Core Algorithms
graph TD
subgraph "1. Token Bucket (Amazon, Stripe)"
TB["Bucket capacity: 10 tokens<br/>Refill rate: 2 tokens/sec"]
TB --> |Has token?| Allow1["Allow (Remove 1 token)"]
TB --> |Empty?| Block1["Block (429)"]
end
subgraph "2. Leaky Bucket (Shopify)"
LB["Queue capacity: 10 reqs<br/>Process rate: 2 reqs/sec"]
LB --> |Queue full?| Block2["Block (Drop req)"]
end
Fixed Window vs Sliding Window:
- Fixed Window: 100 reqs / min. Resets at exactly
12:01:00. (Flaw: A user can send 100 reqs at12:00:59and 100 reqs at12:01:01-> 200 reqs in 2 seconds). - Sliding Window: Looks at the exact trailing 60 seconds from now. Perfectly smooth, but uses more memory.
6. Internal Working
6.1 Token Bucket (The Industry Standard)
Imagine a physical bucket that holds exactly C tokens.
- Every second, we drop R new tokens into the bucket.
- If the bucket is full, extra tokens overflow and are lost.
- When a request arrives, we take 1 token out. If the bucket is empty, we drop the request.
Pros: Allows for bursts of traffic. If you haven't used the API for a while, you can send C requests instantly. Memory efficient.
6.2 Leaky Bucket
Requests pour into the top of the bucket at any rate. They leak out the bottom at a constant rate.
- It's essentially a FIFO queue.
- If the queue is full, new requests are dropped.
Pros: Smooths out traffic bursts. Your backend servers see a perfectly consistent rate of requests. (Shopify uses this for their API).
6.3 Fixed Window Counter
Store a counter in Redis with the key user_42:minute_12:01. Increment it. If it > 100, reject. At 12:02, a new key is used.
Pros: Extremely memory efficient.
Cons: The "Edge of Window" spike problem (2x traffic allowed at the minute boundary).
6.4 Sliding Window Log
Keep a Redis Sorted Set (ZSET) of timestamps for every request a user makes.
When a new request arrives:
- Remove all timestamps older than 1 minute.
- Count the remaining elements.
- If count < limit, add the new timestamp and accept.
Pros: 100% perfectly accurate rate limiting. Cons: High memory usage (must store the timestamp of every single request).
7. Implementation
Why Python + Redis? Redis is the defacto standard for distributed rate limiting. We'll implement the Fixed Window Counter and the Sliding Window Log algorithms using Python.
"""
009 - Distributed Rate Limiter
Demonstrates Fixed Window and Sliding Window algorithms using Redis.
Prerequisites: `pip install redis` and a running Redis server on localhost:6379
"""
import time
import redis
# Connect to Redis
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# ── 1. Fixed Window Counter ──
def is_allowed_fixed_window(user_id: str, limit: int, window_sec: int) -> bool:
"""
Allows 'limit' requests per 'window_sec' chunk of time.
Vulnerable to the edge-of-window burst problem.
"""
# Calculate current time window (e.g., integer division by 60)
current_window = int(time.time() // window_sec)
redis_key = f"rate:fixed:{user_id}:{current_window}"
# INCR is atomic in Redis
current_count = r.incr(redis_key)
# Set expiration on the key so Redis doesn't fill up with old windows
if current_count == 1:
r.expire(redis_key, window_sec * 2)
return current_count <= limit
# ── 2. Sliding Window Log ──
def is_allowed_sliding_window(user_id: str, limit: int, window_sec: int) -> bool:
"""
Allows 'limit' requests in the exact trailing 'window_sec'.
Perfectly smooth, but uses more memory (stores every request timestamp).
"""
redis_key = f"rate:sliding:{user_id}"
now_ms = int(time.time() * 1000)
window_start_ms = now_ms - (window_sec * 1000)
pipeline = r.pipeline()
# 1. Remove all requests older than the window
pipeline.zremrangebyscore(redis_key, 0, window_start_ms)
# 2. Add the current request timestamp
# Using timestamp as both score and value
pipeline.zadd(redis_key, {str(now_ms): now_ms})
# 3. Count total requests in the current window
pipeline.zcard(redis_key)
# 4. Set expiration to keep memory clean
pipeline.expire(redis_key, window_sec * 2)
# Execute transaction atomically
results = pipeline.execute()
# The result of zcard is at index 2
request_count = results[2]
return request_count <= limit
# ── Simulation ──
if __name__ == "__main__":
USER = "user_42"
LIMIT = 3
WINDOW = 5 # seconds
r.flushdb() # Clear redis for testing
print(f"--- Testing Sliding Window ({LIMIT} reqs per {WINDOW}s) ---")
for i in range(1, 8):
allowed = is_allowed_sliding_window(USER, LIMIT, WINDOW)
status = "✅ ALLOWED" if allowed else "❌ BLOCKED (429)"
print(f"Req {i} at {time.strftime('%X')}: {status}")
time.sleep(1) # Send 1 request per second
print("\nWaiting 3 seconds for window to slide...")
time.sleep(3)
allowed = is_allowed_sliding_window(USER, LIMIT, WINDOW)
status = "✅ ALLOWED" if allowed else "❌ BLOCKED (429)"
print(f"Req 8 at {time.strftime('%X')}: {status}")
Sample Output
--- Testing Sliding Window (3 reqs per 5s) ---
Req 1 at 14:00:00: ✅ ALLOWED
Req 2 at 14:00:01: ✅ ALLOWED
Req 3 at 14:00:02: ✅ ALLOWED
Req 4 at 14:00:03: ❌ BLOCKED (429)
Req 5 at 14:00:04: ❌ BLOCKED (429)
Req 6 at 14:00:05: ❌ BLOCKED (429)
Req 7 at 14:00:06: ❌ BLOCKED (429)
Waiting 3 seconds for window to slide...
Req 8 at 14:00:09: ✅ ALLOWED
8. Complexity
| Algorithm | Memory Profile | CPU Overhead |
|---|---|---|
| Fixed Window | Low (O(1) per user/window) | Minimal (1 Redis INCR) |
| Token Bucket | Low (O(1) per user) | Minimal (Math calculation) |
| Sliding Log | High (O(N) where N is rate limit) | Medium (ZSET operations) |
Scalability Characteristics
- Because rate limiting happens on every single request, hitting Redis across the network adds 1-2ms of latency.
- At extreme scale (Millions of RPS), a central Redis cluster becomes a bottleneck. To solve this, companies use Local Cache + Async Sync. The API Gateway caches limits locally in RAM, and syncs to Redis asynchronously (sacrificing strict accuracy for massive performance).
9. Trade-offs
| Strict Accuracy | Memory Usage | Burst Handling | Best For | |
|---|---|---|---|---|
| Fixed Window | ❌ Poor | ✅ Tiny | ❌ Edge spikes | Basic infrastructure limits |
| Sliding Window | ✅ Perfect | ❌ High | ✅ Allowed up to limit | Hard limits on paid APIs |
| Token Bucket | ✅ Good | ✅ Tiny | ✅ Smooth bursts | Stripe / Payment APIs |
| Leaky Bucket | ✅ Good | Medium | ❌ Smooths out bursts | E-commerce checkout queues |
10. Production Evolution
| Concern | This Implementation | Production Systems |
|---|---|---|
| Atomicity | pipeline.execute() | Uses Redis Lua Scripts. A Lua script executes entirely on the Redis server as a single atomic operation, preventing race conditions from concurrent requests. |
| Response Headers | None | Must return X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers so clients know when to retry. |
| Multi-level Limiting | IP or User | Production systems rate limit by IP (to stop DDoS), by User ID (to enforce tiers), and globally (to protect DBs). |
| Fail Open vs Fail Closed | If Redis dies, requests fail | Fail Open: If Redis crashes, let the traffic through. Better to risk a slight overload than to hard-down the entire API for all customers. |
11. Common Bugs
| Bug | What happens | Fix |
|---|---|---|
| Race Conditions | Two requests hit the API at the exact same millisecond. Both read tokens=1, both decrement to 0, both succeed. Limit bypassed. | Use Redis Lua Scripts or Redis atomic operators (INCR) to guarantee atomicity. |
| Edge of Window Burst | Fixed window limit is 100/min. Attacker sends 100 at 12:00:59 and 100 at 12:01:00. Server takes 200 hits in 1 second. | Switch to Sliding Window or Token Bucket. |
| Not returning Retry-After | Legitimate clients get a 429 and immediately spam retries, worsening the load. | Always return a Retry-After: <seconds> header. |
12. Interview Questions
-
Explain the Token Bucket algorithm. Why is it so popular? Hint: It uses very little memory (just two integers: tokens left, last refill time) and it allows for brief bursts of traffic while enforcing an overall average rate.
-
How does a Sliding Window Log differ from a Fixed Window Counter? Hint: Fixed Window stores one counter per time block (e.g., minute). Sliding window stores the exact timestamp of every request. Fixed window allows 2x traffic at window edges; Sliding window is perfectly accurate but uses more memory.
-
Your distributed rate limiter relies on Redis. What happens if the Redis cluster crashes? Hint: The system should "Fail Open". It's better to temporarily lose rate limiting than to block 100% of legitimate traffic because the limiter is down.
-
How do you prevent Race Conditions in a distributed rate limiter? Hint: Do not use a "Read-Modify-Write" pattern in app code. Send a Lua script to Redis to execute the check and decrement atomically.
13. Used By (Downstream Blocks)
- 008 API Gateway — The component that usually executes the rate limiting logic.
- 028 Circuit Breaker — Rate limits protect your servers; circuit breakers protect downstream dependencies.
- 010 Caching — Relies on Redis, just like distributed rate limiters.
14. Used In (Case Studies)
| System | Rate Limit Strategy |
|---|---|
| Stripe | Heavily uses Token Bucket in Redis to protect payment processing APIs. |
| Uses Redis-backed limiters for API endpoints (e.g., 900 read requests per 15 min window). | |
| Discord | Uses a highly granular Bucket system per route, per user, per channel. |
| Shopify | Uses Leaky Bucket (via Nginx and custom code) to ensure checkout queues don't overwhelm backend databases. |
15. Related Blocks
| Relationship | Block |
|---|---|
| Previous | 008 API Gateway |
| Next | 010 Caching Strategies (Understanding Redis) |
16. Try It Yourself
Exercise 1: Sliding Window Approximation (Sliding Window Counter)
Combine Fixed Window and Sliding Window. Keep two counters: the previous minute and the current minute. Calculate the limit dynamically based on the overlap percentage of the current time into the current minute. This yields 99% accuracy with O(1) memory!
Exercise 2: Add Headers
Modify the Python script to return a dictionary containing allowed: bool and retry_after: int. Calculate the retry_after seconds so the caller knows exactly when they are allowed to make the next request.
Website Metadata
| Field | Value |
|---|---|
| Hero Title | Rate Limiter |
| Hero Subtitle | Protect your APIs from abuse. Master Token Bucket, Leaky Bucket, and Sliding Windows. |
| Breadcrumb | System Design → Building Blocks → Rate Limiter |
| Sidebar Category | Tier 1 — Core Backend Components |
| Search Keywords | rate limiter, token bucket, leaky bucket, sliding window, fixed window, redis, ddos protection, api gateway |
| Internal Links | ← 008 API Gateway · → 010 Caching Strategies |
| Suggested Illustration | A funnel controlling a massive waterfall, turning it into a steady, manageable stream filling a cup. |
| Suggested Animation | A Token Bucket animation: coins dropping into a bucket at a steady rate. A request removes a coin. When empty, requests bounce off a red shield. |