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.