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
FailureWhat happens without the mitigation
Client acquires, then crashesThe lock is held until TTL expires - the reason a TTL is mandatory
Work runs longer than the TTLA second client acquires and both write - the reason locks need fences
Client deletes without checking the tokenAnother client's lock is removed
Clock or network stall on the clientThe client may not know it lost the lock
Redis primary fails overA 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
AlgorithmMemoryBursts
Fixed windowOne counter per windowAllows double the rate at a window boundary
Sliding window logOne entry per requestExact, but heavy at high limits
Sliding window counterTwo countersApproximate, cheap, smooths the boundary
Token bucketTwo numbers per keyAllows a burst up to the bucket size
Leaky bucketA queueSmooths 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-After or 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.

Transactions, Lua scripting and atomicity Performance, pipelining and monitoring

Last refreshed 2026-09-18.