RedisDistributed SystemsRust
Implementing a Sliding Window Rate Limiter in Redis
·2 min read
Fixed window rate limiters have a known edge case: a burst at the boundary of two windows lets through twice the limit. Sliding windows solve this.
Why Sorted Sets
Store each request as a sorted
set entry with score = Unix timestamp (ms). To check a new request: remove old entries, count remaining,
if count < limit add and allow.
The Naive Approach Breaks
A pipelined approach batches
commands but isn't atomic. Between ZCARD and ZADD, another request slips through.
Atomic via
Lua
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window =
tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, '-inf',
now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD',
key, now, now)
redis.call('PEXPIRE', key, window)
return 1
else
return 0
end
Redis executes Lua atomically.
From Rust
let result: u8 = Script::new(SCRIPT)
.key(key).arg(now).arg(window_ms).arg(limit)
.invoke_async(&mut conn).await?;
Script::new
computes SHA1 once and uses EVALSHA — no upload cost per call.
Edge Cases
- Namespace keys:
rl:{user_id}:{endpoint} - Pass server time as
now, not client time - On Redis failure: fail open for public APIs
At 85k RPS the bottleneck was connection pool exhaustion — not the Lua script. Pre-warm the pool on startup.