Distributed Patterns
Fencing Tokens: Stopping a Stale Lock Holder From Corrupting State
A client acquires a lease and gets fencing token 33. Then its JVM freezes for 15 seconds on a stop-the-world garbage-collection pause. The lease expires, a second client grabs the lock and gets token 34, and writes some data. The first client thaws, oblivious that it ever lost the lock, and fires off its own write — still stamped 33. The storage server sees a token lower than the 34 it already accepted and rejects the write. No corruption. That rejected write is the whole point of fencing.
A fencing token is a monotonically increasing integer issued by a lock service on every successful acquisition, carried by the client into every protected operation, and checked by the resource, which remembers the highest token it has ever seen and refuses anything older. It converts an unenforceable "please hold the lock" promise into a hard guarantee that a stale holder — a paused process, a partitioned node, a zombie leader — physically cannot mutate shared state.
- TypeDistributed coordination / safety mechanism
- Popularized byMartin Kleppmann, 2016 ("How to do distributed locking")
- Core invariantReject write if token <= highest_seen_token
- Check costO(1) compare-and-store per write
- Used inZooKeeper (zxid/version), etcd (revision), HDFS, Chubby-style leases
- Key ideaMonotonic token makes the resource, not the client, the arbiter of who holds the lock
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
The problem: a lock you hold is not a lock you enforce
Distributed locks make an implicit, dangerous assumption: that a client which was told "you hold the lock" still holds it at the moment it acts. In a single machine that's true — the OS scheduler and the lock word are co-located. Across a network, the gap between "I acquired the lease" and "I am writing" can stretch arbitrarily.
- A stop-the-world GC pause can freeze a JVM for seconds; the lease expires unnoticed.
- A network partition cuts the client off from the lock service while it keeps talking to storage.
- Plain clock skew or a slow syscall stalls the client past the lease deadline.
In every case the lock service, seeing an expired lease, correctly hands the lock to a new client. Now two clients each believe they hold it — a split brain. The original holder, a "zombie," wakes up and writes. Money moves twice, a file is truncated, an invariant breaks. The lock did its job; the assumption that holding it means being allowed to write is what failed. Fencing tokens fix that assumption by making the resource verify, not trust.
How it works: issue, carry, compare
A fencing token is a single strictly-increasing counter with three obligations spread across three parties.
- Issue. On every successful acquire, the lock service returns a token that is strictly larger than any it has ever returned for that resource: 33, then 34, then 35... It must never repeat or go backwards, even across lock-service restarts (so it's persisted or derived from a consensus log).
- Carry. The client attaches the token to every operation it sends to the protected resource — not just the first.
- Compare. The resource keeps
max_seen, the highest token it has ever accepted. On each write it enforces the invariant.
on acquire(resource): # lock service
token = next_monotonic(resource) # 33, 34, 35 ...
return token
on write(resource, token, data): # the resource itself
if token <= max_seen[resource]:
reject("stale token")
else:
max_seen[resource] = token
apply(data)The crucial shift: authority moves from the client's belief to the resource's memory. A zombie's token is, by construction, smaller than the token of whoever acquired the lock after it — so its write is always rejected.
Complexity and a worked step-trace
The enforcement is cheap. Per write the resource does one comparison and one conditional store: O(1) time. Storage overhead is O(number of protected resources) — one integer (max_seen) per resource, typically 8 bytes. Token generation by the lock service is O(1) plus whatever it costs to make the counter durable and ordered (an atomic increment, or one entry in a Raft/Zab log).
A trace of the canonical race, tokens shown in brackets:
t0 Client1 acquire(res) -> token 33 max_seen = 32
t1 Client1 begins long GC pause...
t2 lease(33) expires
t3 Client2 acquire(res) -> token 34
t4 Client2 write(res, 34, X) 34 > 32 -> ACCEPT, max_seen = 34
t5 Client1 wakes, unaware
t6 Client1 write(res, 33, Y) 33 <= 34 -> REJECT
The write with token 33 is refused after 34 has been seen. Note the ordering that matters is at the resource, not wall-clock: even if Client1's packet arrives first, once 34 lands, 33 is permanently dead. The invariant is a per-resource monotone barrier — max_seen only ever climbs.
Where it's used in real systems
You rarely mint tokens by hand; robust systems expose a monotonic counter you can reuse:
- Apache ZooKeeper. The classic lock recipe uses ephemeral sequential znodes; the
czxid(creation transaction id) or a parent'scversionis globally monotonic via the Zab protocol and serves directly as a fencing token. - etcd. Every key mutation gets a Raft-ordered
revision/mod_revision; a client fences with the lock's revision and enforces it via a transactional compare-and-swap at the store. - Google Chubby / lease systems. Chubby's lock "sequencer" — a lock name plus generation number — is the original industrial fencing token; servers validate the sequencer before acting.
- HDFS and HA leaders. An "epoch" or "generation clock" number rejects writes from a deposed NameNode/leader — the same idea under a different name.
The common thread: the token piggybacks on an existing totally-ordered log (Zab, Raft, Paxos), so monotonicity comes for free from consensus rather than from a fragile local clock.
Compared to the alternatives
The alternative to fencing is trusting the lease — and its variants all share one flaw: the resource has no way to tell a live holder from a zombie.
- Plain lease + client trust: simplest, and unsafe under any pause or partition. The client's confidence is not evidence.
- Shorter leases: shrink the danger window but never close it, and short leases mean more renewal traffic and more spurious expirations.
- Redlock (multi-master Redis): Martin Kleppmann's 2016 critique showed its random lock value carries no ordering, so it cannot fence; you'd need a consensus system to generate ordered tokens — at which point you already have what you need.
The tradeoff fencing asks for: the resource must participate. It has to store max_seen and check it on every write. If the resource is a dumb blob store with no conditional-write primitive, you can't fence it directly — you must front it with something that can (a transactional layer, or object-storage conditional/If-Match writes). That cooperation requirement is the price of correctness.
Pitfalls, failure modes, and why it matters
Fencing is simple to state and easy to get subtly wrong.
- Non-monotonic tokens. A random UUID, a wall-clock timestamp (subject to skew and NTP jumps), or a counter that resets on lock-service restart all break the invariant. The token must be strictly increasing and durable across restarts.
- Resource doesn't check. A token nobody validates is decoration. The enforcement must live at the resource, not in client code the zombie is also running.
- Wrong comparison. Using
<instead of<=lets two clients with the same token both write; the check must reject equal-or-lower. - Side effects outside the fence. Fencing protects the write to the fenced store. If the zombie also sends an email or calls a non-fenced API, that action escapes.
- Multiple resources. A single logical operation touching two stores needs the token honored at both, or the fence has a hole.
Its significance is foundational: fencing tokens are how practitioners reconcile the fact that, under the FLP/CAP reality of asynchronous networks, you cannot prevent a client from thinking it holds a lock — so instead you make its stale actions harmless. It is the small idea that makes distributed mutual exclusion actually safe.
| Approach | Produces monotonic token? | Stale-holder write blocked? | Notes / tradeoff |
|---|---|---|---|
| Lease + trust the client | No | No | Client believes it holds the lock; a GC pause or partition silently voids that belief |
| Redlock (Redis multi-master) | No | No | Random lock value is not ordered; Kleppmann showed it cannot fence without added consensus |
| ZooKeeper lock + zxid / znode version | Yes | Yes | czxid or cversion is globally monotonic via Zab; natural fencing token, needs ZK cluster |
| etcd lock + mod_revision | Yes | Yes | Raft-ordered revision is monotonic; compare-and-swap enforces at the store |
| Fencing token (generic pattern) | Yes | Yes (if resource checks it) | Only works if the resource persists max token and rejects lower ones |
Frequently asked questions
Why not just use shorter lease timeouts instead of fencing tokens?
Shorter leases only shrink the window in which a paused or partitioned client can act stale; they never eliminate it, because a GC pause or network delay can always exceed any finite timeout. Shorter leases also increase renewal chatter and cause more false expirations of healthy clients. Fencing closes the window entirely by making the resource reject stale writes regardless of timing.
Does Redlock provide fencing tokens?
No. Redlock produces a random lock value that has no ordering, so a resource cannot tell which of two lock values is newer. Martin Kleppmann's 2016 analysis showed you would need a genuine consensus system just to generate monotonic tokens — and if you have that, you no longer need Redlock's multi-master scheme for safety.
What makes a good source of fencing tokens?
Anything strictly monotonic and durable across restarts. In practice you reuse a consensus-ordered counter: ZooKeeper's zxid/czxid or znode version, etcd's mod_revision, or a Raft/Paxos log index. Avoid wall-clock timestamps (clock skew, NTP jumps) and random UUIDs — neither is ordered — and avoid any counter that resets when the lock service restarts.
Where exactly is the token checked — client or server?
At the resource (the server/storage layer being protected), never in client code. A zombie client is running the same code as a healthy one, so any client-side check it could pass, the zombie passes too. Only the resource, which persists the highest token it has accepted and rejects anything not strictly greater, can distinguish a live holder from a stale one.
What is the performance cost of fencing?
Minimal. Each protected write costs one integer comparison against a stored max_seen and a conditional update — O(1) time. Space is one integer (about 8 bytes) per protected resource. Token issuance adds one durable/ordered increment at the lock service, which is usually amortized into an existing consensus log entry.
What can fencing tokens NOT protect against?
Anything outside the fenced resource. If the stale client sends an email, charges a card via a non-fenced API, or writes to a second store that doesn't validate the token, those effects escape. Fencing only guarantees that writes to a cooperating resource that checks the token are serialized by lock generation; non-idempotent side effects and multi-resource operations need the token honored everywhere or additional safeguards.