A distributed lock coordinates exclusive access to a resource across multiple processes or machines, the same way a mutex coordinates threads inside one process, except the failure modes are worse, because processes crash, networks partition, and clocks drift independently of each other. The core guarantee you actually want is simple to state and surprisingly hard to deliver: at most one caller holds the lock at any moment, and a caller that has lost the lock cannot keep acting as though it still holds it. Almost every real incident with distributed locks traces back to that second half being ignored.
How it works
The basic protocol:
1. Client asks: "give me the lock for resource X, for 30 seconds"
2. Lock service grants it (or refuses if already held)
3. Client does its work
4. Client releases the lock (or it expires after 30s if the client never returns)
import redis
r = redis.Redis()
acquired = r.set("lock:invoice-42", "worker-7", nx=True, ex=30) # SET NX EX: only if unset, 30s TTL
if acquired:
try:
process_invoice(42)
finally:
r.delete("lock:invoice-42") # release
else:
# someone else holds it; retry later or skip
pass
This basic version has a real gap: if process_invoice runs longer than 30 seconds, the lease expires while the worker is still running, another worker acquires the same lock, and now two workers are processing invoice 42 concurrently.
Why a TTL alone is not enough
The fix is a fencing token, a monotonically increasing number handed out with each lock grant. The resource being protected, whether a database row, a file, or a downstream API, checks that the token is higher than the last one it saw, and rejects writes from a stale holder even if that holder still believes it holds the lock.
| Approach |
Protects against |
Fails against |
| Lock with no TTL |
Nothing on crash, hangs forever |
Any crash or forgotten release |
| Lock with TTL only |
Crashed holders, lease expires |
A paused holder that resumes after expiry and still writes |
| Lock with TTL and fencing token |
Stale writes after expiry |
Nothing, if the protected resource enforces the token |
Without fencing, a TTL only bounds how long you wait for a crashed holder. It does not stop a slow-but-alive holder from acting after its lease is gone.
Choosing an implementation
| Option |
Consistency model |
Good fit |
Single Redis instance (SET NX EX) |
Best-effort, can lose the lock on failover |
Non-critical, low-stakes coordination |
| Redlock (multiple Redis instances) |
Stronger, still debated for edge cases |
Medium-stakes, already running Redis |
| etcd or ZooKeeper |
Strong, backed by a proven consensus algorithm |
High-stakes coordination, leader election |
Database row lock (SELECT ... FOR UPDATE, advisory lock) |
As strong as your database |
Already have a relational database in the path, such as protecting a migration from running twice |
For most application-level coordination, such as preventing a scheduled job from double-running or deduplicating a webhook handler, a database advisory lock or single Redis instance is enough. Reach for etcd or ZooKeeper when the cost of two holders acting at once is genuinely severe, such as a financial double-spend or split-brain writes.
Common mistakes
No TTL on the lock. If the holder crashes before releasing, the lock is held forever and every other caller is blocked indefinitely. Always set an expiry.
No fencing token. A TTL bounds the wait but does not stop a delayed, still-alive holder from writing after its lease has effectively passed to someone else, as shown in the table above.
Treating "I have the lock" as "I will finish before it expires." Long-running work under a lock needs either a TTL with generous headroom, a lock-renewal heartbeat mechanism, or a redesign to shrink the critical section.
Using a distributed lock where a database transaction would do. If everything you're protecting lives in one database, a transaction or SELECT ... FOR UPDATE is simpler and just as correct; reserve a dedicated lock service for coordination that spans multiple systems.
FAQ
Is Redlock safe?
It's debated. Redlock is reasonable for reducing the chance of concurrent access in non-critical paths, but a well-known critique from Martin Kleppmann shows it can fail under clock jumps or long pauses. For correctness-critical locking, use a consensus-backed store, such as etcd or ZooKeeper, with fencing tokens instead.
What's the difference between a distributed lock and leader election?
They're related mechanisms with different lifetimes. A lock is typically held briefly for one operation; leader election elects one long-lived coordinator that stays leader until it fails or steps down.
Can I just use a database unique constraint instead of a lock?
For many cases, yes. A unique constraint plus a transaction achieves the same "only one winner" outcome without a separate lock service, and it's simpler to reason about.
How long should a lock's TTL be?
Long enough to comfortably cover the expected work, short enough that a crash doesn't block others for an uncomfortable amount of time. Pair a modest TTL with heartbeat renewal for work whose duration is variable.
Where to go next