Designing a Distributed Lock

A system design interview guide to mutual exclusion and single-leader selection across many machines — making sure that only one process at a time holds a lock or acts as the leader, even when nodes crash, pause, or get cut off from the network.

Student edition · start with the guarantee

Coordinate work without letting expired, paused holders corrupt protected resources.

Correctness contract: Grants are uniquely identified; renew/release compare atomically. Protected effects enforce a durable fence or equivalent ownership condition.

Basic HTTP, databases, and programming are assumed. Specialized terms are explained below. Named products motivate the problem; this is an educational design, not a claim about their current internals. Capacity numbers are illustrative decimal payload estimates, not benchmarks. Add indexes, protocol overhead, replicas, headroom, and failure capacity before provisioning.

On a single machine, mutual exclusion is a solved problem: the operating system gives you a mutex, and the hardware guarantees that only one thread holds it. Spread the same job across many machines and that guarantee evaporates. There is no shared memory to put the mutex in, the machines can fail independently, and their clocks do not agree. A distributed lock tries to recreate "only one holder at a time" on top of that hostile environment, and a closely related problem — leader election — is just the special case of "exactly one machine should be in charge." This guide explains why these are genuinely hard, builds a lock from a store with a lease, adds fencing tokens to make it safe, walks through the famous pitfalls of naive lock implementations, and then covers leader election and split-brain avoidance.

Contents

  1. Requirements & Clarifying Questions
  2. Back-of-Envelope Estimation
  3. API & Data Model
  4. Why It Is Hard
  5. A Lock with a Lease
  6. Fencing Tokens
  7. Naive Locks and Redlock
  8. Renewal and Heartbeats
  9. Leader Election
  10. Split-Brain Avoidance
  11. Tradeoffs
  12. Failure Modes
  13. A 45-Minute Interview Walkthrough
  14. Common Mistakes
  15. Summary
  16. Check Yourself
  17. Key Terms
  18. Further Reading
  19. Concepts to understand first
  20. Worked example
  21. Runnable Python model

Concepts to understand first

Each definition includes a concrete example and the condition under which the mechanism is useful. You can read this guide without opening another guide.

Leases

Time-limited permission issued by an authority. Expiry lets new work proceed after a client stops renewing; it does not prove that the previous holder is dead or unable to act.

Example. A 30-second lease expires while A is paused for 45 seconds. B can acquire a new lease, but A can still wake up and send a delayed write.

Limit. Heartbeats improve recovery time, not safety by themselves. Compare-and-renew/delete must verify a unique acquisition token atomically, and protected effects need fencing or equivalent serialization.

Fencing tokens

A strictly increasing grant number attached to operations. The protected resource atomically compares it with its durable highest accepted token and rejects older operations, making superseded clients unable to overwrite newer effects.

Example. A has token 33 and pauses. B acquires 34 and writes. When A sends token 33 later, the resource rejects it because its fence is already 34.

Limit. A new grant must be installed at the resource before it promises exclusion there. Token 33 can still pass until 34 is observed. Equal-token operations also need sequence/idempotency rules if they can arrive out of order.

Quorum

A set of replicas whose responses are sufficient for an operation. In a fixed set of N replicas, read size R and write size W overlap whenever R+W>N. A majority is one particular quorum, not the definition of every quorum.

Example. With N=3, writes to {A,B} and reads from {B,C} share B. If the write completed durably and replicas preserve a comparable version, B carries that version forward.

Limit. Overlap alone does not provide linearizability: concurrent or failed writes, sloppy replica sets, and version selection matter. Consensus adds protocol rules to preserve decisions.

Raft terms and commit rules

Raft elects at most one leader per logical term and replicates an ordered log. To advance commit by counting replicas, the leader requires a majority storing an entry from its current term; earlier entries then commit as part of that prefix.

Example. A new leader commits a current-term no-op before relying on old-term prefix entries. Log up-to-date voting and matching rules preserve committed history.

Limit. Terms are not fixed time intervals. Old leaders from earlier terms can remain unaware. Durable term/vote state, membership rules, read barriers, and client-request dedup are required.

Optimistic concurrency and compare-and-set

Update state only if the expected version or status still matches, in the same atomic operation that changes it. A failed comparison means a conflicting operation won; reread or return a conflict.

Example. UPDATE seats SET status='held', version=version+1 WHERE id=? AND status='available' allows only one concurrent claimant to update the row.

Limit. Application-level GET then unconditional SET is not equivalent. Include acquisition identity to reject stale callbacks, and couple related invariants in a transaction.

Wall and monotonic clocks

A wall clock maps to civil/epoch time and can jump because of correction. A monotonic clock measures elapsed time locally without wall-clock jumps, but its origin is not a shared global epoch.

Example. Use an epoch timestamp to label an event and a monotonic deadline to bound how long a process waits for the next usable timestamp.

Limit. Neither establishes causal order across machines. Monotonic readings cannot be compared between hosts or assumed to survive reboot with the same origin.

1. Requirements & Clarifying Questions

"Design a distributed lock" is vague on purpose. Ask these first:

FunctionalNon-functional
acquire(key, ttl) returns a lease and a fencing token, or failsSafety: at most one holder's writes take effect at a time
release(key, lease) frees only your own lockLiveness: a crashed holder's lock frees itself within the TTL
renew(key, lease) extends a live leaseLow latency: a few ms to acquire
Leader election: exactly one leader per group, auto failoverSurvives the loss of a minority of lock-service nodes
Intuition

A lease is a hotel key card that stops working at checkout time. If the guest vanishes, the room frees itself. But a guest who oversleeps may still try the old card. A fencing token is the lock on the door checking the card's issue number, and refusing any card older than the newest one it has seen.

2. Back-of-Envelope Estimation

Take a busy job system: 50K acquires/s, each lock held about 10 s, TTL 30 s with renewal every 10 s.

StepWorkingResult
Locks held at once50K/s × 10 s (Little's law)500K locks
Renewals500K locks ÷ 10 s50K/s
Releasesone per acquire50K/s
Memory500K × ~100 B per key~50 MB
Total lock-store ops50K acquire + 50K renew + 50K release150K ops/s
What the numbers tell you

150K small ops/s and 50 MB fits a small sharded Redis cluster with ease. A consensus store like etcd or ZooKeeper sustains roughly tens of thousands of writes per second, because every write goes through a quorum and an fsync. So use consensus for coarse, important locks such as leader election, and do not put per-request locks on it. If per-request locks must be correct, fence at the database instead.

3. API & Data Model

acquire(key, ttl_ms, wait_ms=0) -> { lease_id, fencing_token, expires_at } | LOCKED
renew(key, lease_id, ttl_ms)    -> { expires_at } | LOST
release(key, lease_id)          -> OK | NOT_OWNER

# one record per lock
lock_key       string   # e.g. "order:1842"
owner          string   # random lease id, unique per acquire
fencing_token  int64    # strictly increasing per key
expires_at     time     # judged by the lock service's clock only

Clients should not compare expires_at with their own wall clock. Clocks drift. A client should track time left with a local monotonic timer that starts before it sends the request, so it always underestimates what it has.

4. Why It Is Hard

The difficulty of distributed locking comes from three facts about distributed systems that simply are not true on one machine. Any design that ignores them will look correct in a demo and corrupt data in production.

Put together, these mean you can never build a distributed lock that is simultaneously perfectly safe and perfectly live. The honest goal is a lock that is safe by construction — it never lets two holders both successfully mutate the protected resource — and reasonably live, recovering automatically when a holder dies. The mechanisms in the rest of this guide are all in service of that "safe even under partial failure" property.

A useful interview framing: the hard part is never the happy path of acquire-then-release. It is the moment a holder pauses for several seconds (a long GC pause, a VM freeze) and wakes up still believing it owns a lock that has since expired and been handed to someone else.

5. A Lock with a Lease

The first building block is to put the lock in a shared store (a key-value store, a database, a coordination service) and give it a lease: a time-to-live after which the lock automatically expires. A client acquires the lock by atomically creating a key only if it does not already exist, with a TTL attached. If the holder dies without releasing, the key expires on its own and the lock becomes available again — no human has to clean up after a crashed process.

The lease is what makes the lock live in the face of partial failure. Without it, a holder that crashes would keep the lock forever and the whole system would deadlock. The TTL is, in effect, the system's answer to "how long do we wait before assuming the holder is dead?" Set it too short and a slow-but-alive holder loses its lock; set it too long and recovery after a real crash is sluggish. There is no perfect value, which is exactly why the lease alone is not enough for safety — and why fencing tokens, covered next, are needed.

# acquire: atomic create-if-absent with a TTL lease
function acquire(lock_key, owner_id, ttl):
  ok = store.set_if_absent(lock_key, owner_id, expire=ttl)
  return ok          # true only if we created the key

function release(lock_key, owner_id):
  # must be ONE atomic step: compare the owner, then delete
  store.compare_and_delete(lock_key, expected=owner_id)
Common mistake: get, then delete

Writing release as if get(key) == me: delete(key) in two calls has a race. Your lease can expire between the two calls, and another client can take the lock. Your delete then removes their lock. The check and the delete must happen as one atomic step. In Redis, run them together in a Lua script:

-- Redis: EVAL this with KEYS[1]=lock key, ARGV[1]=owner id
if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
else
  return 0
end

The owner id must be unique per acquire (a random token), not just the host name. Otherwise a restarted process on the same host could release a lock that a different run of it took.

6. Fencing Tokens

A durable authority issues a monotonic, nonreused grant token. The resource atomically compares it with its durable highest accepted token and applies the effect. Check and write cannot be separate calls.

UPDATE resource SET data=:data, fence=:token
 WHERE id=:id AND fence <= :token;
-- Check affected rows; persist fence and effect together.

Activation boundary: granting 34 does not instantly update a separate resource. Old 33 can pass until the new fence is installed there. If exclusion begins at the new grant, synchronize resource activation or check authoritative ownership on each effect. Fencing orders recognized grants, not proof of unexpired lease validity.

Equal tokens support multiple holder writes, but delayed same-holder operations need their own ordering/idempotency. A random release token is not a monotonic fence. etcd revisions and ZooKeeper recipes require proper transaction/session scope and nonreuse across restored/recreated namespaces; do not assume every counter is globally eternal.

7. Naive Locks and Redlock

A very common interview answer is "just use a single Redis key with SET key value NX PX ttl." That is a fine starting point and works for the happy path, but it has two well-known weaknesses. First, on a single Redis node the lock is only as durable as that node — if the primary fails over to a replica that had not yet received the lock write, two clients can hold the "lock" at once. Second, and more fundamentally, a single-key lock provides no fencing token, so it cannot survive the paused-holder scenario from the previous section.

The Redlock algorithm was proposed to address the durability gap by acquiring the lock on a majority of several independent Redis nodes, so the loss of any one node does not lose the lock. With five nodes, the client tries SET NX PX on each in turn with a short timeout. It holds the lock only if at least three said yes and the whole attempt took less than the TTL. Its remaining validity is the TTL minus the time spent, minus an allowance for clock drift. If it fails, it releases on every node. Redlock then became the subject of a famous debate. Critics argued that it still relies on bounded clock drift and bounded pauses for its safety guarantee, and that a lock used to protect a resource must be backed by fencing tokens regardless of how many nodes you spread it across — at which point the multi-node ceremony buys you little for correctness. The practical takeaway for an interview is not which side won, but the underlying lesson:

Redlock: ask 5 independent nodes, need 3 ClientSET NX PX 30000 Redis 1: OK Redis 2: OK Redis 3: OK Redis 4: taken Redis 5: timeout 3 of 5 = majority validity = 30s − elapsed − drift allowance
Two clients cannot both get three of five nodes, so a majority stops a double grant while every node stays up. It still gives no fencing token, and it trusts that pauses and clock drift stay small. That is the heart of the Redlock debate.

8. Renewal and Heartbeats

A fixed TTL forces an awkward tradeoff: long-running work needs a long lease, but a long lease means slow recovery after a crash. The way out is lease renewal. The holder runs a background heartbeat that periodically extends the lease while it is still alive and making progress, so you can keep the TTL short (fast recovery) while still supporting work that runs much longer than one TTL.

The heartbeat is also how the system detects death. As long as renewals keep arriving, the holder is presumed alive and keeps the lock. The instant they stop — because the holder crashed, hung, or was partitioned — no further extensions occur, the lease runs out, and the lock is freed for someone else. This same heartbeat-or-expire pattern is exactly what leader election uses, which is why the two topics are so closely linked.

Lease timeline with renew versus expiry
Top: a healthy holder renews the lease before each TTL elapses, so it keeps the lock indefinitely. Bottom: the holder dies after one renewal, no further renewals arrive, the TTL elapses, and the lock auto-expires so another node can acquire it.
# background heartbeat keeps a short lease alive while we work
function heartbeat_loop(lock_key, owner_id, ttl):
  while still_working:
    sleep(ttl / 3)                      # renew well before expiry
    ok = store.extend(lock_key, owner_id, ttl)
    if not ok:                          # we lost the lock
      stop_work()                       # do not keep mutating
      break

Like release, extend must be an atomic compare-and-extend: only push the expiry out if the key still holds our owner id. In Redis, that is the same Lua pattern with PEXPIRE in place of DEL.

Common mistake

Thinking heartbeats remove the need for fencing. A stop-the-world GC pause freezes the whole process, heartbeat thread included. The lease runs out while the worker is frozen, and the worker cannot notice until it wakes, by which time another holder may be writing. Renewal makes expiry rarer. Only fencing makes it safe.

9. Leader Election

Use a reviewed coordination service recipe rather than treating standalone Redis SET-NX as consensus. A session/lease expires at its authority, but a paused holder may remain unaware. Raft terms are logical epochs, not fixed TTL intervals; timeouts suspect failure rather than prove death.

A new leader commits only through quorum/log rules. Protect downstream effects with installed monotonic fences and linearizable reads with current-authority/applied-state barriers. Lease loss should stop cooperative work as an extra defense, not the sole safety proof.

10. Split-Brain Avoidance

Raft elects at most one leader per term. An older-term partitioned leader can remain unaware while a newer-term leader exists. Safety comes from durable election/log rules: the old side cannot commit conflicting history without a quorum. Linearizable reads require current authority and applied state, not merely is_leader.

Quorums apply to the agreed membership configuration and protocol. Odd counts are common because 2f+1 tolerates f unavailable nodes; an even count is not inherently unsafe. Membership changes follow the coordinator's safe transition protocol. Fence external job/storage effects, which are not automatically covered by the consensus log.

11. Tradeoffs

How much machinery you need depends entirely on what the lock protects. An interviewer wants to see you match the mechanism to the cost of getting it wrong.

MechanismBuys youCost / limitation
Single-key lock with TTLSimple, fast, fine for best-effort coordination.No safety under failover or paused holders; no fencing.
Lease + heartbeat renewalLiveness: dead holders are reclaimed, long jobs supported.Still unsafe on its own without fencing; tuning TTL is a tradeoff.
Fencing tokensSafety: stale holders cannot corrupt the resource.The protected resource must be modified to check tokens.
Consensus / coordination serviceCorrect election and locking under partitions; auto-failover.Operationally heavier; adds latency and another system to run.
Redlock (majority of Redis nodes)Survives one node loss better than a single key.Relies on bounded pauses and clock drift; still no fencing token.
Database row lock (SELECT … FOR UPDATE)Safe if the work is in the same database transaction.Holds a DB connection; only protects that database.
No lock: idempotent or conditional writesNothing to fail; often the simplest correct answer.Needs a natural key or version column to compare on.

The pragmatic rule: if a brief double-acquire merely wastes work (two workers process the same job, idempotently), a simple lease lock is fine. If a double-acquire corrupts data or money, you need fencing tokens and, for leadership, a consensus-backed coordination service with majority quorum. Reach for the heavy machinery only when the cost of being wrong justifies it.

12. Failure Modes

What failsWhat happensHow to handle it
Holder crashesLock stays takenTTL expires it; keep the TTL short and renew
Holder pauses past its TTL (GC, VM freeze)Two holders believe they own itFencing token checked at the resource
Redis primary fails before replicating the lockNew primary grants the lock againFencing; or a consensus store; Redis WAIT helps but is not a guarantee
Network partitionMinority side cannot reach the lock serviceMinority holders stop work when renewal fails; quorum stops a second leader
Clock jumps on a lock nodeKeys expire early or lateUse monotonic clocks for expiry; fence regardless
Release sent after the lease expiredDeletes someone else's lockAtomic compare-and-delete on the owner id
Hot lock, many waitersThundering herd on every releaseWatch only your predecessor (ZooKeeper recipe) or back off with jitter

Worked example: writer 33 returns after writer 34

A pauses beyond its lease; B receives a new grant.

Worked example, in orderLease 33 expires: The authority may issue grant 34, but it has not physically stopped A.. B installs fence 34: The resource atomically records 34 with its authorized activation/effect. Highest accepted fence is now 34.. A returns with token 33: An atomic check rejects 33<34 before modifying the resource.. Order B's repeated writes: Equal grant tokens permit multiple writes only with appropriate same-holder sequencing/idempotency. A fence alone does not order them.1Lease 33 expires2B installs fence 343A returns with token 334Order B's repeated writes
Read top to bottom. The table below explains the state and guarantee at each step.
StepState, calculation, and reasoning
1. Lease 33 expiresThe authority may issue grant 34, but it has not physically stopped A.
2. B installs fence 34The resource atomically records 34 with its authorized activation/effect. Highest accepted fence is now 34.
3. A returns with token 33An atomic check rejects 33<34 before modifying the resource.
4. Order B's repeated writesEqual grant tokens permit multiple writes only with appropriate same-holder sequencing/idempotency. A fence alone does not order them.
Check your understanding: Is A fenced just because B received 34 from the lock service?

Not at a separate resource until 34 is installed/observed there, or each effect checks current authority. Make the activation boundary explicit.

Run a small, testable model

Download concepts.py. It uses only the Python 3.10+ standard library. From the directory where you saved it, run:

python3 concepts.py fencing
python3 concepts.py test

The model tests stale-token rejection and the boundary before a newly granted token reaches the resource.

The program contains deterministic assertions and failure traces. It is a learning model, not a production service: it does not implement network consensus, distributed durability, authentication, or third-party delivery. Read its contracts and try changing an input before extending it.

Primary references for these concepts

13. A 45-Minute Interview Walkthrough

MinutesWhat to do
0–5Ask: efficiency or correctness? Hold time? Rate? Can the resource check a token?
5–10Estimate locks held, ops/s and memory. Use the result to pick Redis or a consensus store.
10–20Build it up: set-if-absent with TTL, owner id, atomic release, renewal.
20–30Break it: the GC pause story. Fix it with fencing tokens and show the atomic check.
30–38Leader election and split-brain: majority quorum plus fencing. Mention Redlock and the debate.
38–45Tradeoffs table; say when you would avoid a lock with idempotent or conditional writes.
Interview tip

The strongest single sentence you can say: "A lock gives liveness; fencing at the resource gives safety." Then tell the GC-pause story in four steps. Most candidates never get there.

14. Common Mistakes

15. Summary

A distributed lock recreates mutual exclusion on top of a system with no shared memory, independent failures, and unsynchronized clocks. The design is a stack of mechanisms, each covering a gap the previous one leaves open:

ConcernMechanism
Why is this hard?No shared memory, partial failure, and clock skew — you cannot tell "dead" from "slow."
How do we reclaim a crashed holder's lock?Store the lock with a TTL lease so it auto-expires.
How do we stop a paused, stale holder corrupting data?Fencing tokens — monotonically increasing numbers the resource enforces.
Why isn't a naive Redis lock enough?Single-node failover can double-grant, and it offers no fencing; Redlock's safety is debated.
How do we support long jobs with short TTLs?Heartbeat renewal — extend the lease while alive, free it the moment renewals stop.
How do we pick a single leader?Leader election via a consensus protocol or coordination-service ephemeral nodes with heartbeats.
How do we recover when the leader dies?Heartbeats stop, the lease expires, the rest re-elect — automatic failover.
How do we avoid two leaders?Majority quorum prevents dual election; fencing tokens stop a stale leader from corrupting state.
The recurring theme: liveness and safety are separate problems. Leases and heartbeats give you liveness — the system recovers from crashes. Fencing tokens and majority quorum give you safety — even a confused, paused, or partitioned holder cannot corrupt the protected resource. A correct design needs both halves.

16. Check Yourself

  1. Why is if get(k) == me: delete(k) unsafe as a release?
    Answer

    The lease can expire between the get and the delete. Another client then takes the lock, and your delete removes their lock. The compare and the delete must be one atomic step, such as a Lua script in Redis.

  2. Client 1 holds token 33 and freezes. Client 2 gets token 34 and writes. Client 1 wakes and writes. What must the storage do, and how?
    Answer

    Reject Client 1's write, because 33 is lower than the highest token seen (34). It must check and write atomically, for example UPDATE … WHERE fence <= :token.

  3. A lock is held for 10 s on average and acquired 50K times per second. How many locks are held at once, and how many renewals per second if each lock renews every 10 s?
    Answer

    Little's law: 50K × 10 = 500K locks held. Each renews once every 10 s, so 500K ÷ 10 = 50K renewals/s.

  4. Why does a cluster of 5 nodes tolerate 2 failures but a cluster of 4 tolerates only 1?
    Answer

    A majority of 5 is 3, so 2 can fail. A majority of 4 is also 3, so only 1 can fail. The fourth node adds cost but no fault tolerance, which is why clusters use odd sizes.

  5. A worker renews its lease every 10 s with a 30 s TTL. Why can it still end up writing after it lost the lock?
    Answer

    A long GC pause or VM freeze stops the whole process, heartbeat thread included. If the pause lasts over 30 s, the lease expires and another worker takes over. The frozen worker wakes up and writes before it notices. Only a fencing check at the resource stops that write.

  6. When is a plain single-Redis-key lock good enough?
    Answer

    When it is an efficiency lock: a rare double grant only wastes work, and the work is idempotent. Example: stopping two workers from rebuilding the same cache entry.

17. Key Terms

Mutual exclusion
At most one holder at a time.
Lease
Time-limited permission issued by an authority. Expiry lets new work proceed after a client stops renewing; it does not prove that the previous holder is dead or unable to act. Example and limits above.
TTL
Time to live: how long until the lease expires.
Fencing token
A strictly increasing grant number attached to operations. The protected resource atomically compares it with its durable highest accepted token and rejects older operations, making superseded clients unable to overwrite newer effects. Example and limits above.
Compare-and-delete
Delete a key only if it still holds the expected value, atomically.
Redlock
Acquire on a majority of independent Redis nodes.
Quorum
A set of replicas whose responses are sufficient for an operation. In a fixed set of N replicas, read size R and write size W overlap whenever R+W>N. A majority is one particular quorum, not the definition of every quorum. Example and limits above.
Split-brain
Two sides of a partition each believe they lead.
Ephemeral node
A ZooKeeper node deleted when its client session ends.
Efficiency vs correctness lock
Double grant wastes work vs corrupts data.
Liveness / safety
Something good eventually happens / nothing bad ever happens.

18. Further Reading


⌂