Distributed rate limiting fails when checking capacity and recording admission are separate operations.
Atomic Sliding-Window Limit
Admission and accounting must be one atomic decision at the scarce-resource boundary.
local key, now, window, limit = KEYS[1], ARGV[1], ARGV[2], tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
if redis.call('ZCARD', key) >= limit then return 0 end
redis.call('ZADD', key, now, now .. '-' .. ARGV[4])
redis.call('PEXPIRE', key, window)
return 1Invariant: Admission and accounting happen atomically for one principal and window.
Use when: A distributed API needs a per-principal rate limit without race conditions.
Why this boundary matters
Separate check and increment operations race under concurrency. Atomic execution ensures every admitted request consumes exactly one place in the window.
Failure policy
| Boundary | Action |
|---|---|
| Below limit | Admit and account atomically |
| Limit reached | Return 429 with a useful Retry-After |
| Redis unavailable | Apply an explicit fail-open or fail-closed policy per endpoint |
| Clock skew possible | Use one trusted time source |
| Key is inactive | Expire its state to bound memory |
Trade-offs
Sliding windows are fairer than fixed windows but cost more memory and operations. A centralized limiter gives consistent decisions but becomes a dependency on the admission path.
Decision rule: Rate-limit on the identity and resource that represent the scarce capacity, with failure behavior chosen by abuse and availability risk.