Distributed Lock
A distributed lock grants mutual exclusion across processes through a shared store, but correctness requires leases, ownership checks, and fencing tokens.
Definition
A distributed lock is a lease stored in a shared system such as Redis, etcd, or a database. A holder acquires the lease for a limited time and releases it or lets it expire. Mutual exclusion is only as strong as the lease and fencing. A process that pauses past its lease can still believe it holds the lock, so storage must reject writes carrying an older fencing token.
Context and scope
Distributed locks coordinate singleton work such as a scheduled job, a migration, or a per-account operation. They are not a substitute for database constraints or idempotency when the protected resource itself can detect conflicts.
Common misconceptions
- A lock guarantees the holder is the only actor — a paused holder can resume after the lease expires
- A TTL makes the lock safe — it enables progress but creates a window for a second holder
- Redis or etcd alone provides correctness — the protected resource must enforce fencing or version checks
Anti-patterns
- Deleting a lock key without checking that the caller owns it
- Using a long TTL and no renewal, so a crashed holder blocks everyone
- Protecting a database write with a lock but no fencing token
Related concepts
Related examples
Authoritative source
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
A lock is a lease with a failure window
The holder acquires a key with a short TTL and a unique owner token. Other contenders fail to acquire until the key expires or is released. If the holder crashes, the TTL lets the system make progress. If the holder pauses—GC, VM freeze, network delay—it may resume after the TTL and still believe it owns the lock. Another process now owns it too.
const token = crypto.randomUUID();
const acquired = await redis.set('lock:account:42', token, 'PX', 30_000, 'NX');
if (!acquired) return { acquired: false };
// Release only if the token still matches.
await redis.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
1,
'lock:account:42',
token
);
The compare-and-delete release prevents deleting another holder’s lock. It does not prevent a stale holder from writing to the protected resource. That requires fencing.
Fencing is the safety mechanism
Give each acquisition a monotonically increasing token and include it in every write to the protected resource. The resource rejects a token lower than the highest it has seen. A stale holder may still think it owns the lock, but its writes fail. A database sequence, a consensus store, or an object version can provide the token; a random UUID cannot be ordered.
Renewal and liveness
Long tasks need a watchdog that renews the lease before expiry, and the watchdog must stop when the task is cancelled or the process is unhealthy. A lock with no renewal expires during a legitimate long operation, allowing a second holder. A lock with a very long TTL blocks progress after a crash. Choose a TTL based on task duration and renewal cadence, and test the pause scenario.
When to use it
Use a distributed lock for coordination where duplicate execution is harmful and the protected resource cannot enforce exclusivity itself. The advantage is cross-process mutual exclusion; the drawback is lease timing, clock assumptions, and the need for fencing. Compared to a database unique constraint or optimistic version check, a lock is more complex and easier to get wrong; prefer the resource-level guard when it is available.