Distributed locks and rate limiting
Correct SET NX PX locks, the token that makes release safe, the real caveats of Redlock, and token bucket and sliding-window limiters.
A lock that actually works
# acquire: atomic set-if-absent with an expiry and an owner token
SET lock:order:42 <token> NX PX 30000
# release: compare and delete, in one atomic step (see the Lua script earlier)
# never: DEL lock:order:42 -- you may delete someone else's lock
# extend only if you still hold it: renewal, not acquisition
# repeated in a heartbeat while the work continues
# a fencing token is the only real protection
# every write to the protected resource carries the token,
# and the resource rejects any token lower than the highest seen| Failure | What happens without the mitigation |
|---|---|
| Client acquires, then crashes | The lock is held until TTL expires - the reason a TTL is mandatory |
| Work runs longer than the TTL | A second client acquires and both write - the reason locks need fences |
| Client deletes without checking the token | Another client's lock is removed |
| Clock or network stall on the client | The client may not know it lost the lock |
| Redis primary fails over | A lock held on the old primary may be granted again on the new one |
⚠️
A distributed lock built on Redis is not a correctness guarantee under failover: an asynchronous replica may not have received the
SET before the primary dies, so two clients can hold the lock. Use it to reduce duplicated work, and use fencing tokens or a conditional write on the resource itself for correctness.Rate limiters
-- sliding window log: precise, memory proportional to the limit
-- KEYS[1] = key, ARGV[1] = now ms, ARGV[2] = window ms, ARGV[3] = limit
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2])
local count = redis.call('ZCARD', KEYS[1])
if count >= tonumber(ARGV[3]) then
return 0
end
redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1] .. '-' .. math.random(1000000))
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1-- token bucket: O(1) memory, allows bursts up to the bucket size
-- KEYS[1] = bucket, ARGV[1] = now ms, ARGV[2] = rate/s, ARGV[3] = burst
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1]) or tonumber(ARGV[3])
local ts = tonumber(data[2]) or tonumber(ARGV[1])
local now = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
tokens = math.min(tonumber(ARGV[3]), tokens + (now - ts) / 1000 * rate)
if tokens < 1 then
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', ts)
return 0
end
redis.call('HSET', KEYS[1], 'tokens', tokens - 1, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(tonumber(ARGV[3]) / rate * 1000) * 2)
return 1| Algorithm | Memory | Bursts |
|---|---|---|
| Fixed window | One counter per window | Allows double the rate at a window boundary |
| Sliding window log | One entry per request | Exact, but heavy at high limits |
| Sliding window counter | Two counters | Approximate, cheap, smooths the boundary |
| Token bucket | Two numbers per key | Allows a burst up to the bucket size |
| Leaky bucket | A queue | Smooths output, adds latency |
- The limiter must be atomic or two concurrent requests both read the same count and both pass. A script is the simplest way to guarantee that.
- Key the limiter by the identity that matters: user id for fairness, IP for abuse, or both, with a stricter rule for anonymous traffic.
- Always set an expiry on the limiter key. A limiter without a TTL leaks one key per identifier forever.
- Return a retry hint (
Retry-Afteror a reset timestamp) so clients can back off instead of hammering the endpoint.
FAQ
Is Redlock safe?
It is a reasonable best-effort lock for reducing duplicated work, and its correctness under clock skew, long pauses and failover has been convincingly criticised. If two clients holding the lock at once can corrupt data, the protection must live in the resource - typically a conditional write or a fencing token.
Which rate limiter should I start with?
A fixed or sliding window counter keyed by user id, with a hard TTL. It is simple, cheap and predictable. Move to a token bucket when legitimate clients need to burst.
Related
Transactions, Lua scripting and atomicity Performance, pipelining and monitoring
Last refreshed 2026-09-18.