Distributed rate limiting with Redis across multiple servers
distributed rate limiting, Redis atomic operations, MULTI/EXEC, Lua scripts in Redis, race conditions in distributed systems, consistency trade-offs
Distributed rate limiting with Redis across multiple servers
The Problem with Local Rate Limiting
If you store rate limit counters in each server's memory and you have 10 servers behind a load balancer, each server allows 100 req/min — your actual limit becomes 1,000 req/min per client. Local rate limiting is useless at horizontal scale.
Redis as Shared State
Store all counters in Redis. Every server reads and writes to the same counter. Use atomic operations to prevent race conditions:
-- Lua script: atomic check-and-increment
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
local count = redis.call('INCR', key)
if count == 1 then
redis.call('EXPIRE', key, ttl)
end
if count > limit then
return 0
end
return 1// Call from Node.js
const allowed = await redis.eval(luaScript, 1, key, 100, 60);Lua for Atomicity
Redis executes Lua scripts atomically — no other command runs between steps. This eliminates the TOCTOU (time-of-check to time-of-use) race condition that exists if you use separate INCR and EXPIRE calls. Always use Lua or MULTI/EXEC pipelines for multi-step rate limit operations when multiple instances share a counter.
