66

A distributed lock without a fencing token is broken

#
  • pattern
  • established
  • fragment

No TTL-based lock (Redis, ZooKeeper lease, k8s lease) protects anything on its own: the holder can stall (a GC pause, a network delay), the TTL expires, someone else acquires the lock — and the “dead” holder wakes up and writes over them. The only correct scheme: the lock hands out a monotonic token, and the storage rejects writes carrying a stale token. The protection lives on the resource side, not the lock side — this is the E2E argument (21) applied to locking.

Code example (fragment)
fragment
Go
token, err := locker.Acquire(ctx, "settlement-2026-08-04") // token = 34
if err != nil { return err }
defer locker.Release(token)

// the store checks: accepted max token = 33 → 34 passes;
// a zombie arriving with token = 33 after 34 was seen is rejected
if err := store.Write(ctx, key, val, WithFenceToken(token)); err != nil { ... }
Rust
// the token is in the type: a write can't be built without it
pub struct Fenced<T> { token: FenceToken, payload: T }

impl Store {
    pub fn write(&self, w: Fenced<Write>) -> Result<(), Stale> { ... }
}

An honest assessment. The requirement on the resource is more precisely stated as a monotonic check: a fencing token is one special case, and a version or a conditional write / compare-and-swap expresses the same protection. If the resource supports none of these (S3 without a precondition, an external API), the lock buys you efficiency — not doing the work twice — but not correctness; confusing these two uses is the other half of “distributed lock” incidents, including the well-known Kleppmann vs antirez debate over Redlock. And separately: consensus (Raft/Paxos) is an algorithm, not a heuristic; you do not write your own implementation, you take an existing one.

Last technical review: 2026-08-04 · Reference · 112 principles