An in-depth engineering blueprint for designing production-ready distributed rate limiters. Compares Token Bucket, Sliding Window Counter algorithms, Redis Lua script implementations, handling race conditions, and interview tradeoffs.
Rate limiting is an essential component of backend architecture. It controls the rate of traffic sent by a client or service, shielding API endpoints, databases, and third-party integrations from resource exhaustion.
N tokens. Tokens are continuously added at a fixed refill rate (e.g., 10 tokens per second). When an HTTP request arrives, it consumes 1 token. If no tokens remain, the request is rejected with HTTP 429.Current Window Weight = Previous Window Count * (1 - Current Time Offset / Window Size) + Current Window Count
In a distributed infrastructure with multiple API gateway instances, local in-memory rate limiters fail because state isn't shared. A centralized Redis store solves state distribution.
To prevent race conditions when two requests arrive simultaneously at separate API servers, we execute the check and increment atomically inside a Redis Lua script:
-- Redis Lua Script for Sliding Window Rate Limiter
-- KEYS[1]: Rate limit key (e.g. "rate_limit:user_123")
-- ARGV[1]: Current Unix Timestamp (milliseconds)
-- ARGV[2]: Window duration in milliseconds (e.g. 60000)
-- ARGV[3]: Max requests allowed in window (e.g. 100)
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local clearBefore = now - window
-- 1. Remove expired timestamps outside the sliding window
redis.call('ZREMRANGEBYSCORE', key, 0, clearBefore)
-- 2. Count requests currently in the window
local currentRequests = redis.call('ZCARD', key)
if currentRequests < limit then
-- Add current request timestamp
redis.call('ZADD', key, now, now)
-- Set TTL on the set for auto cleanup
redis.call('PEXPIRE', key, window)
return {1, limit - currentRequests - 1} -- Allowed (1)
else
return {0, 0} -- Blocked (0)
end
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
export async function checkRateLimit(
userId: string,
limit = 100,
windowMs = 60000
): Promise<{ allowed: boolean; remaining: number }> {
const key = `ratelimit:${userId}`;
const now = Date.now();
const luaScript = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local clearBefore = now - window
redis.call('ZREMRANGEBYSCORE', key, 0, clearBefore)
local currentRequests = redis.call('ZCARD', key)
if currentRequests < limit then
redis.call('ZADD', key, now, now)
redis.call('PEXPIRE', key, window)
return {1, limit - currentRequests - 1}
else
return {0, 0}
end
`;
const [allowed, remaining] = (await redis.eval(
luaScript,
1,
key,
now.toString(),
windowMs.toString(),
limit.toString()
)) as [number, number];
return {
allowed: allowed === 1,
remaining: Math.max(0, remaining),
};
}
When designing a Rate Limiter in a high-level system design interview:
X-RateLimit-Limit: Maximum requests per window.X-RateLimit-Remaining: Remaining allowed requests.X-RateLimit-Reset: Unix timestamp when the window resets.Retry-After: Seconds to wait before retrying (on HTTP 429).