Distributed Locks and Fencing Tokens

Distributed locks explained: lease-based locks, why process pauses break them, fencing tokens, and when to avoid locking entirely.

Advanced 3 min read · Updated

A distributed lock ensures that only one process, across many machines, works on a shared resource at a time: processing an order, renewing a certificate, running a migration. Unlike a lock inside one process, it must survive crashes, network partitions and pauses, which makes "exactly one holder" much harder than it sounds.

Two reasons to lock

  • Efficiency: avoid doing the same work twice (two workers generating the same report). If the lock occasionally fails, you waste some work. A simple lock is fine.
  • Correctness: two holders would corrupt data or break an invariant (double-spending, two writers to one file). Here the lock must be safe even when processes pause, which requires fencing.

Lease-based locks

Distributed locks are leases: they expire automatically, so a crashed holder can't block everyone forever.

# Acquire: set only if absent, with a 30-second expiry and a unique token
SET lock:invoice:981  "worker-7:uuid-4f2c"  NX  PX 30000

# Release: delete only if we still own it (atomic check-and-delete in a Lua script)
if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
end
return 0
  • Unique owner value: so a process never deletes a lock someone else now holds (after its own expired).
  • Expiry longer than the work, with renewal (a "watchdog" extending the lease) for long tasks.
  • Retry with backoff and jitter when the lock is taken, rather than spinning.

Why expiry alone isn't safe

Any lease-based lock has the same flaw: the holder can pause after acquiring the lock (a garbage-collection pause, a CPU-starved container, a slow disk) for longer than the lease. The lock expires, another process acquires it, and when the first wakes up, two processes believe they hold the lock. Checking "do I still hold it?" right before writing doesn't help, because the pause can happen between the check and the write.

Client 1Lock serviceClient 2Storageacquiregranted, token 41client 1 paused past its leaseacquiregranted, token 42write with token 42write with token 41rejected (41 < 42)
Fencing tokens: storage remembers the highest token it has seen and rejects writes carrying an older one, so a paused former holder can't corrupt data.

A fencing token is a number that increases every time the lock is granted. The protected resource stores the highest token it has accepted and rejects lower ones. This requires the resource to participate (a database WHERE version < ? check, a storage API that accepts tokens). ZooKeeper's zxid and etcd's revision numbers serve as fencing tokens.

Choosing a lock service

OptionSafetyNotes
Single Redis SET NX PXEfficiency onlyFast and simple; a Redis failover can lose the lock
Redlock (multiple Redis nodes)Debated for correctnessStill timing-dependent; add fencing for correctness
etcd / ZooKeeper / ConsulStrong (consensus-backed) with fencing tokensRight choice for correctness-critical locks. See consensus
Database row lock / advisory lockStrong within that databaseSELECT … FOR UPDATE or pg_advisory_lock; simplest when the resource is in the same DB

Often you don't need a lock

  • Database constraints and conditional writes: a unique constraint or UPDATE … WHERE status = 'pending' is atomic and safe without any lock. See isolation levels.
  • Optimistic concurrency: version numbers detect conflicts at write time.
  • Partition ownership: assign each entity to one worker (Kafka partitions keyed by ID), so no two workers ever touch the same entity.
  • Idempotency: if doing the work twice is harmless, a lock only saves effort. See idempotency.

Where it’s used

Cron jobs on many instances

A Redis or database lock ensures a scheduled job runs on one instance per period (an efficiency lock).

Google Chubby

A Paxos-based lock service used for master election and coordination in Bigtable and GFS; it issues sequencers that act as fencing tokens.

HBase and Kafka (historically)

Relied on ZooKeeper locks and ephemeral nodes for region and controller ownership.

Database migrations

Migration tools take an advisory lock so only one deployment runs schema changes at a time.

In an interview

  • Ask whether the lock is for efficiency or correctness, and choose accordingly.
  • Use a lease with expiry, a unique owner token, and safe release.
  • For correctness, add fencing tokens checked by the resource, and explain the process-pause problem.
  • Prefer avoiding locks with conditional writes, unique constraints or partition ownership.