Why your Redis lock is probably not safe
The correct implementation, its real guarantees, and when it is not enough.
Open this lesson in the learning hubKey points
- The naive lock -
SETNXthenDEL- has two independent bugs. Without a TTL a crashed holder locks the resource forever; with a TTL, a slow holder can have its lock expire and then delete someone else lock. - The fix for the second is a fencing token: store a unique value with the lock and release it only if the value still matches, atomically in a Lua script.
- Even correct, this is a lease, not a lock. If the holder pauses - a GC pause, a scheduling delay, a network partition - the lease expires and a second holder starts while the first still believes it holds it.
- That is a real failure, not a theoretical one. A 200ms GC pause against a 5-second lease is fine; a 6-second pause is not, and nothing in Redis prevents it.
- Redlock across several independent nodes is widely debated, and the debate matters: it does not make the lease safe under process pauses, only under node failure.
- The honest conclusion: use a Redis lock for efficiency - preventing duplicate work - and never for correctness. If double execution would corrupt data, you need a fencing token the resource itself checks, or a database transaction.
Example
# BROKEN: no TTL. A crashed holder locks it forever.
> SETNX lock:order:42 1
# STILL BROKEN: TTL, but anyone can delete anyone lock.
> SET lock:order:42 1 NX EX 30
# ... holder is slow, lock expires, B acquires ...
> DEL lock:order:42 # A deletes B lock, and now two holders
# CORRECT acquire - unique token, atomic with the TTL:
> SET lock:order:42 "a1b2c3-unique-token" NX EX 30
# CORRECT release - compare and delete, atomically in Lua:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0 -- not ours any more; do NOT delete
end
// In Java:
String token = UUID.randomUUID().toString();
boolean acquired = Boolean.TRUE.equals(redis.opsForValue()
.setIfAbsent("lock:order:42", token, Duration.ofSeconds(30)));
if (acquired) {
try {
doWork();
} finally {
// compare-and-delete, never a bare DEL
redis.execute(RELEASE_SCRIPT, List.of("lock:order:42"), token);
}
}
/*
* WHAT THIS STILL DOES NOT PREVENT:
*
* t=0 A acquires, lease 30s
* t=5 A pauses (GC, CPU starvation, VM migration)
* t=30 lease EXPIRES - Redis has no idea A is alive
* t=31 B acquires the same lock and starts working
* t=35 A resumes, still believing it holds the lock
* -> TWO holders, and A has no way to know
*
* A lease cannot be made safe against an arbitrary pause. So:
*
* EFFICIENCY (fine) avoid two workers doing the same import
* CORRECTNESS (not) "only one process may debit this account"
*
* For correctness, the RESOURCE must reject the stale writer - a
* monotonic fencing token it stores and compares, or a database
* transaction with optimistic locking.
*/
A Redis lock is a lease that a process pause can silently break - use it to avoid duplicate work, never to protect correctness.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Redis Course course, and every lesson in it is listed on the Redis Course contents page.