DevTools Hub

Search tools

Search for a developer tool

Distributed Systems

Distributed Lock Simulator

Simulate a lease-based distributed lock race and see whether a fencing token stops the stale write.

Part of the Distributed Systems Toolkit
Data corrupted
Client A's pause outlasted its lease. Client B acquired the lock and wrote, then Client A resumed and overwrote it with stale data — with no fencing token, the resource had no way to tell.
TimeActorEventDetail
t=0msClient AAcquires lockLease TTL 5000ms
t=0msClient ABegins work, then pausesUnplanned pause of 8000ms (GC, VM live migration, or the OS not scheduling it)
t=5000msResourceClient A's lease expiresClient A is still paused and has no way of knowing time passed
t=5000msClient BAcquires lockLease TTL 5000ms
t=5000msClient BWritesResource accepts the write
t=5000msClient BReleases lock
t=8000msClient AResumes, unaware its lease expiredStill believes it holds the lock and attempts its write
t=8000msResourceAccepts Client A's stale writeNo fencing token to check against — the resource has no way to tell this write is stale

What this simulates

A lease-based distributed lock — acquire with a TTL, the lock auto-expires if you don't renew it — has a real hole: nothing stops a client's process from pausing (garbage collection, VM live migration, the OS simply not scheduling it) for longer than the TTL after acquiring the lock but before finishing its work. This tool simulates exactly that race: Client A acquires the lock, pauses for however long you set, and Client B acquires the lock the moment Client A's lease expires — then you can see whether a fencing token catches Client A's stale write when it finally resumes.

Why a longer TTL isn't the fix

It's tempting to set the TTL to something comfortably longer than any pause you'd expect, and the Safe preset shows why that works — right up until a pause genuinely unbounded in length happens anyway. A GC pause, a hypervisor migration, or a scheduler simply not running a process are all real, observed causes of multi-second (or longer) pauses that no timeout you pick in advance can rule out. The TTL only changes howlikely the race is, not whether it's possible — which is exactly Martin Kleppmann's 2016 critique of Redis' Redlock algorithm: the only way to be safe against this class of failure is for the protected resource itself to reject stale writes, not to make the pause less likely.

How fencing tokens close the hole

A fencing token is a monotonically increasing number handed out every time the lock is granted. The protected resource — not the lock service — tracks the highest token it has seen and rejects any write carrying a lower one. In the Race, with fencing preset, Client A's stale write still arrives after Client B has already written with a higher token, but the resource recognizes it's stale and refuses it. ZooKeeper's sequential ephemeral znodes and etcd's MVCC revision number both give you this property natively, which is why they're the usual recommendation over a lock primitive that doesn't hand out a comparable, ordered token at all.

FAQ

Is anything sent anywhere?

No — this is a deterministic, client-side simulation. Nothing leaves your browser.

Does this model a real Redis, ZooKeeper, or etcd deployment?

No — it models the abstract mechanism common to all lease-based locks: a TTL, a pause, and whether the resource checks a token. Real systems add their own details (Redlock's multi-instance quorum, ZooKeeper's watches, etcd's lease API) on top of this same underlying hazard. See Distributed Locks Explained for how each system actually implements fencing.

Why does Client B acquire the lock at exactly the TTL, instead of some time after?

That's the worst case, and the one worth designing for — a competing client polling aggressively (or itself racing to acquire the instant it sees the lease has expired) can realistically acquire it immediately. Modeling any gap would understate the risk.

What's a fencing token in practice — do I have to build one myself?

If your lock service hands out something monotonically increasing and comparable already (a Snowflake ID, a ULID, or a plain counter), that's a usable fencing token — see Distributed ID Explorer for generating and comparing exactly that shape of value. What matters is that the resource being protected actually checks it against the highest one it's seen, which the lock service alone can't enforce for you.

Related tools