Overview
A rate limiter controls how many requests a client can make in a time window. It protects services from abuse, prevents resource exhaustion, and enforces fair usage in multi-tenant systems.
Interview scope: Design a rate limiter that:
- Handles 10,000 RPS across 10M users
- Works across multiple API gateway nodes (distributed)
- Allows different limits per user tier (free: 100 RPM, paid: 1000 RPM)
- Returns
429 Too Many RequestswithRetry-Afterheader
Architecture
Client Requests
│
▼
┌────────────────────────────────────────────────┐
│ API Gateway Cluster │
│ │
│ Node 1 ──▶ Rate Limit Check ──▶ Forward/429 │
│ Node 2 ──▶ Rate Limit Check ──▶ Forward/429 │
│ Node 3 ──▶ Rate Limit Check ──▶ Forward/429 │
│ │ │
│ │ Lua script (atomic) │
└───────────────────│─────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ Redis Cluster │
│ │
│ Key: rate:{userId}:{window} │
│ TTL: window size (60s) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Shard 1 │ │ Shard 2 │ │
│ │ user:1..3M │ │ user:3M..6M │ ... │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ Downstream Services │
│ (only reached if rate limit not exceeded) │
└──────────────────────────────────────────────────┘
Response Headers (on every request):
┌─────────────────────────────────────────────┐
│ X-RateLimit-Limit: 100 │
│ X-RateLimit-Remaining: 43 │
│ X-RateLimit-Reset: 1714600800 │
│ Retry-After: 47 (only on 429) │
└─────────────────────────────────────────────┘
Algorithms
Token Bucket (Recommended for most cases)
Bucket has capacity C tokens.
Tokens refill at rate R per second.
Each request consumes 1 token.
If bucket is empty → reject.
Pros: allows short bursts up to C
Cons: burst can hammer downstream if C is large
Sliding Window Counter (Interview favorite — explain this one)
Window: last 60 seconds, split into 1-second slots.
Count requests in each slot.
Sum slots within the window.
If sum > limit → reject.
time: 0 1 2 3 4 5 ... 58 59 60
─ ─ ─ ─ ─ ─ ─ ─ ─
count: 2 0 3 1 0 4 ... 2 1 ← current slot
Current window sum = sum(slots[now-60..now]) = 13
More accurate than fixed window, less memory than per-request log.
Fixed Window (Simple but has burst problem)
Window: current minute (e.g., 12:00:00 – 12:01:00)
Count resets at window boundary.
Problem: user can send 100 at 12:00:59 and 100 at 12:01:00
→ 200 requests in 2 seconds, despite 100/min limit.
Technical Implementation
Redis Lua Script — Atomic Sliding Window
-- rate_limit.lua
-- KEYS[1] = rate:{userId}:{windowStart}
-- ARGV[1] = current timestamp (ms)
-- ARGV[2] = window size (ms)
-- ARGV[3] = limit
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
-- Remove counts older than window
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- Count current window
local current = redis.call('ZCARD', key)
if current >= limit then
return {0, current, limit} -- rejected
end
-- Add current request
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('PEXPIRE', key, window)
return {1, current + 1, limit} -- allowed
// src/middleware/rate-limiter.ts
import { createClient } from 'redis';
import fs from 'fs';
const redis = createClient({ url: process.env.REDIS_URL });
const script = fs.readFileSync('rate_limit.lua', 'utf-8');
export async function checkRateLimit(
userId: string,
tier: 'free' | 'paid'
): Promise<{ allowed: boolean; remaining: number; resetMs: number }> {
const limits = { free: 100, paid: 1000 };
const windowMs = 60_000; // 1 minute
const [allowed, current, limit] = await redis.eval(
script,
{ keys: [`rate:${userId}`], arguments: [Date.now().toString(), windowMs.toString(), limits[tier].toString()] }
) as [number, number, number];
return {
allowed: allowed === 1,
remaining: Math.max(0, limit - current),
resetMs: windowMs,
};
}
// Express middleware
export const rateLimitMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const userId = req.user?.id ?? req.ip;
const tier = req.user?.tier ?? 'free';
const { allowed, remaining, resetMs } = await checkRateLimit(userId, tier);
res.setHeader('X-RateLimit-Limit', tier === 'paid' ? 1000 : 100);
res.setHeader('X-RateLimit-Remaining', remaining);
res.setHeader('X-RateLimit-Reset', Math.floor((Date.now() + resetMs) / 1000));
if (!allowed) {
res.setHeader('Retry-After', Math.ceil(resetMs / 1000));
return res.status(429).json({ error: 'Rate limit exceeded' });
}
next();
};
Interview Preparation
Q: How do you handle the distributed case — multiple API nodes?
All nodes share Redis. The Lua script runs atomically on Redis — no race conditions. Redis is the single source of truth for counts. If Redis is down, fail open (allow) or fail closed (reject) — configurable per SLA.
Q: What if Redis goes down?
Strategy 1: Fail open — allow all requests. Abuse gets through temporarily, but service stays up. Right for most APIs. Strategy 2: Local fallback — each node tracks counts locally. Approximate, allows 10× the limit if 10 nodes. Wrong for strict billing. Strategy 3: Circuit breaker — if Redis is down > 5s, return 503 to the client ("service unavailable"). Right for billing/compliance.
Q: Fixed window vs sliding window — what's the bug in fixed window?
Burst at window boundary. A user sends 100 requests at 11:59:59 and 100 at 12:00:01. Both windows count 100 (under limit), but the service got 200 in 2 seconds. Sliding window prevents this by counting over a true rolling period.
Q: How do you rate limit by IP when behind a load balancer?
Use X-Forwarded-For header (last IP before your server, or the first in the chain depending on trust). Configure your load balancer to set this reliably. Be careful — headers are easily spoofed by clients not behind your LB.
Q: How do you implement different rate limits for different endpoints?
Composite key: rate:{userId}:{endpoint}:{window}. /api/search gets 50 RPM, /api/upload gets 5 RPM. Endpoint limits defined in config, looked up in the middleware before the Redis call.
Learning Resources
- Stripe Rate Limiting Blog — production patterns from Stripe
- Redis Rate Limiting — atomic Redis patterns
- Cloudflare: How We Built Rate Limiting — at internet scale
- System Design Interview Vol. 1 — Alex Xu, Chapter 4