A single-process mutex works because the OS enforces mutual exclusion directly on shared memory — there's no gap where two threads can both believe they hold it. A "distributed lock" can't make that same promise, and the reason isn't primarily network unreliability — it's something easy to forget matters at all: a process can pause for an unbounded amount of time (garbage collection, VM live migration, the OS simply not scheduling it for a while) and have no way of knowing how long it was gone. Martin Kleppmann's 2016 critique of Redis' Redlock algorithm made exactly this point concrete, and it's the single most important thing to understand before reaching for any distributed locking library.
The failure, concretely: a pause outlives the lock
A lease-based lock — acquire with a TTL, the lock auto-expires if you don't renew it — is the obvious design, and it has a real hole: nothing stops a client's process from pausing for longer than the TTL after acquiring the lock but before finishing its work. The client has no way to know time passed while it was frozen.
Both A and B write to the same resource, each believing it's the exclusive holder. Neither the network nor the lock service did anything wrong — the pause alone was enough.
The actual fix: fencing tokens
The lock service alone can't close this hole, because the problem isn't really about who holds the lock — it's about the protected resource accepting a write from a client that no longer should be trusted. The fix (the term comes from Kleppmann's post; the underlying idea — a monotonic "sequencer" — already appears in Google's Chubby paper, Burrows, 2006) is a fencing token: every time the lock is granted, the lock service hands out a monotonically increasing number along with it. Every write to the protected resource has to carry that token, and the resource itself — not the lock service — rejects any write whose token is lower than the highest one it's already seen.
Same pause, same TTL expiry — but the resource itself enforces the token, so Client A's stale write is rejected instead of silently corrupting data.
This is also why fencing only works if the resource being protected can actually check and reject a token — a plain file on a shared filesystem generally can't. It has to be something that can hold the "last seen" state and compare: a database row with a token column and a conditional update, an object store with conditional writes, or a service you control that checks the token on every request.
Redlock, and the debate around it
Redis' own answer to distributed locking, Redlock, acquires a lock by getting a majority of independent Redis instances to agree within a bounded time budget. Kleppmann's critique specifically targeted Redlock: it has no fencing-token concept, so it's vulnerable to the exact pause scenario above, and its safety also leans on assumptions about bounded clock drift and timing that are hard to actually guarantee in a real deployment. This became a well-known public disagreement — Redis creator Salvatore Sanfilippo (antirez) published a rebuttal arguing Redlock is safe enough for what it's actually meant for: reducing duplicate work efficiently, not serving as the sole mechanism preventing data corruption. Both sides of that debate are correct about their own scope — the disagreement was really about what a distributed lock is being asked to guarantee in the first place.
When a simple lock is genuinely fine
Not every use of a lock needs airtight fencing. A plain Redis SET ... NX PX or a database row with a locked_until column is a perfectly reasonable choice when the cost of an occasional double-execution is genuinely low — a scheduled job trigger meant to avoid running the same cron job twice, where the job itself is naturally idempotent anyway, doesn't need a fencing token; a duplicate run is a wasted bit of compute, not a correctness bug. Reach for fencing tokens — or a lock service built with them in mind — specifically when a duplicate writer would cause real damage: two processes both believing they're allowed to write to the same file, or two nodes both believing they're the leader.
Systems that get this right
ZooKeeper's standard locking recipe uses sequential ephemeral znodes: each client creates a znode with a server-assigned, strictly increasing sequence number, the client with the lowest number holds the lock, and everyone else watches the next-lowest node for deletion instead of polling. That sequence number is a fencing token, built into the primitive rather than bolted on. etcd gives the same property natively through its MVCC revision number, which increases monotonically with every write and is the documented mechanism for fencing etcd-based locks. Both trace back to the same idea Google's Chubby shipped first: a lock service is only actually useful for correctness if it hands out something that lets the resource it's protecting tell an old lock holder from the current one.
Distributed locks and leader election are the same problem at different timescales — electing a leader is acquiring a lock that's held for a long time instead of one critical section. The identical hazard applies: a deposed leader that paused right as it lost leadership and doesn't know it yet is a "zombie leader," and fencing it off the same way is exactly how real consensus systems (ZooKeeper- and etcd-backed leader election included) avoid two nodes both acting as leader at once.
Try it yourself
Distributed ID Explorer generates exactly the shape of value a fencing token needs — monotonically increasing and comparable. A Snowflake ID or a ULID's embedded timestamp demonstrates the same ordering property a real fencing token relies on: generate a few in sequence and the values sort in creation order every time, which is the entire mechanism a resource uses to tell a stale write from a current one.
FAQ
Is Redlock unsafe to use at all?
Not for what it's actually good at — reducing duplicate work efficiently, where an occasional double-execution is an inefficiency, not a correctness bug. It's the wrong tool specifically as the sole safeguard against data corruption, which is a narrower and stricter requirement than most systems reaching for "a distributed lock" actually have.
Does using ZooKeeper or etcd automatically make my lock safe?
No — they correctly hand out a monotonically increasing token, but a fencing token only protects a resource that actually checks it. If the service on the other end of a write doesn't compare the incoming token against the highest one it's seen and reject stale ones, the lock service did its job perfectly and the write still corrupts data anyway.
What's the difference between a distributed lock and leader election?
Mostly duration and framing, not mechanism — leader election is a lock held for an extended period, where the "critical section" is "acting as the leader" instead of one short operation. The pause hazard and the fencing-token fix apply identically to both.