Home › High-Level Design › Distributed Locks
D

Distributed Locks

A lock on one machine is a solved problem: threads share memory and a CPU gives you compare-and-swap. The moment two machines need to agree who gets to touch a resource, every easy assumption disappears and you are in the hardest part of distributed systems. I have watched strong candidates glide through a whole design and then come apart on one question: "how do you make sure only one worker processes this job?" Answer that cleanly and you signal you understand failure, not just happy paths.

High-Level DesignAdvanced11 min read

If you remember five things

The whole topic in one glance. Everything below defends these five calls.

  1. A lock is a lease, not a guarantee. You rent exclusivity for a bounded window, and the service can declare you dead while you are merely slow.
  2. The pause is the enemy. A GC pause, a VM migration, or a network stall can freeze you past your TTL, and now two clients both think they own the lock.
  3. Fencing tokens make it correct. A monotonic number checked at the resource rejects the slow client's late write. This is the one idea that actually fixes split ownership.
  4. Efficiency vs correctness picks your stack. If a rare double-grant is survivable, one Redis lock is fine. If it corrupts data, you need fencing plus a consensus store.
  5. A strong lock trades availability. Consensus needs a quorum, so a partition stalls acquisition. That is CAP showing up in your lock, and you should say it out loud.

1The core idea#

Start with what a distributed lock actually promises, because the mental model decides everything downstream.

The hotel-key picture.

  • A distributed lock lets processes on different machines agree that only one holds a resource at a time.
  • Picture one bathroom key at a hotel front desk: you cannot enter unless you hold the physical key.
  • The key is the coordination point. Everyone trusts the desk, not each other.

Why a mutex does not translate.

  • In one process a mutex works because threads share memory and atomic instructions like compare-and-swap.
  • Across machines you have none of that: no shared memory, no shared clock, and a network that drops, delays, and reorders.
  • So the lock lives somewhere both parties can reach and trust: a strongly consistent store (ZooKeeper, etcd, Redis), sometimes a plain database row.

The model I want you carrying into the room: a lock is a lease.

  • You are not holding a lock forever. You are renting exclusivity for a bounded window, say 30 seconds.
  • The whole difficulty comes from one brutal fact: the service can think you are dead when you are just slow.
  • A 200ms GC pause, a network hiccup, a 4 second live-migration, and your lease expires while you still believe you own it.
  • Two clients now think they hold the same lock. That is the failure mode that keeps people up at night, and I keep coming back to it.
The one sentence to internalize

A distributed lock does not guarantee exclusivity. It grants a time-boxed lease, and time is the thing you cannot trust across machines. Every correct design treats "my lease may have already expired" as a fact, not an edge case.

2How it works#

The acquire protocol is short. The release protocol is where people cut themselves.

Acquire: set-if-absent with a TTL.

  • A client asks the lock service to set a key only if it does not already exist, with a time-to-live attached.
  • In Redis that is one atomic command: SET lock_key random_value NX PX 30000.
  • NX means "only if not set." PX 30000 means "expire in 30 seconds."
  • Command succeeds: you hold the lock. Command fails: someone else has it.

The TTL is the safety net.

  • If your client crashes while holding the lock, the key expires on its own and the next client grabs it.
  • Without a TTL, one dead process blocks everyone forever.

Release is more dangerous than it looks.

  • You cannot just delete the key. By release time your lease may have expired and someone else may own the lock.
  • Deleting then would stomp on their lock.
  • So you store a random value on acquire, and on release you delete only if the stored value still matches yours.
  • That check-and-delete must be atomic, which is why real code uses a small Lua script, not a read-then-delete round trip.
  • A read-then-delete has a window between read and delete where the lease can expire and get reassigned, and you delete the wrong owner's key.

Here is the acquire path and the dangerous gap:

sequenceDiagram participant C1 as Client One participant L as Lock Store participant C2 as Client Two C1->>L: SET lock NX PX 30000 L-->>C1: OK you hold it Note over C1: long GC pause L->>L: TTL expires C2->>L: SET lock NX PX 30000 L-->>C2: OK you hold it Note over C1,C2: both think they own the lock C1->>L: write to resource C2->>L: write to resource
The split-ownership window. Client One never did anything wrong. It paused, its lease silently expired, and the lock store happily handed the same lock to Client Two. Both now write to the resource, and nothing in this diagram stops them.

The fix for split ownership is a fencing token.

  • Every grant hands out a number that only ever goes up: 33, then 34, then 35.
  • The client passes that token to the actual resource it is protecting, say a storage service.
  • The resource remembers the highest token it has seen and rejects anything lower.
  • So when paused Client One wakes with token 33 and tries to write, the storage already saw 34 from Client Two and says no.
  • The slow client gets fenced out. This is the single most important idea in the topic, and most candidates have never heard of it.
flowchart LR A[Client] --> B[Lock Service] B --> C[Service with resource] C --> D[(Storage)] C --> E[(Cache)]
Where the token has to live. The lock service issues the fencing token, but the check happens at the resource (storage and cache), not at the lock. If the resource cannot reject a stale token, no lock in front of it can make the write safe.
From the interviewer's chair

When I say "walk me through release," I am watching for whether you notice your lease might already be gone. Candidates who just DEL the key have never been burned. The compare-value-then-delete with a Lua script, said unprompted, tells me you have shipped one of these.

3A worked example with real numbers#

Numbers make the pause problem concrete, so here is one I have actually debugged.

The setup.

  • A nightly billing job must charge each customer exactly once.
  • The service runs on 10 pods for availability.
  • The job takes about 90 seconds on a normal night.

First attempt, naive. Each pod tries SET billing_lock <uuid> NX PX 120000, a 120 second TTL to cover the 90 second runtime with margin. Pod 3 wins and starts charging. Then the timeline that bit us:

  • t = 0s: Pod 3 acquires the lock, TTL expires at t = 120s.
  • t = 85s: Pod 3 hits a stop-the-world GC pause plus a noisy-neighbor CPU throttle on the node. It freezes.
  • t = 120s: the lock TTL expires. Redis frees the key. Pod 3 is still frozen and has no idea.
  • t = 121s: Pod 7 runs SET ... NX, succeeds, and starts the billing job from the top.
  • t = 140s: Pod 3 thaws, believes it still holds the lock, and resumes charging the remaining customers.

The blast radius.

  • Every customer charged after t = 121s got billed twice.
  • With 50,000 customers and the pause hitting at roughly 70% through the run, that is about 15,000 duplicate charges.
  • That is a Sev 1 and a refund fire drill, not a rounding error.
~15,000
Duplicate charges from one pauseA 120 second TTL felt generous against a 90 second job. One 35 second freeze blew straight through the margin and double-charged a third of the book. The lock was "working" the entire time.
Pause before you read on: the obvious fix is a bigger TTL. Why does that not save you?
Reveal the answer
Because pauses are unbounded. A bigger TTL just moves the cliff: it means a crashed holder blocks everyone for longer, and a long enough pause (or a chain of them) still outlives any fixed number you pick. You cannot choose a TTL large enough to be safe. You have to make the resource reject the late write, which is fencing.

Now add fencing.

  • The lock service issues a monotonically increasing token with each grant.
  • Pod 3 got token 41. Pod 7 got token 42.
  • The payment ledger records the highest token it has committed, now 42.
  • When Pod 3 thaws and sends a charge stamped with token 41, the ledger sees 41 < 42 and rejects it.
  • Zero duplicate charges, even though two pods genuinely believed they held the lock at the same time.

The lesson in one line.

  • The lock reduced contention. Only the fencing token at the resource made the system correct.

4Trade-offs#

The real decision is where the lock lives. Each choice buys you something and charges you something else.

Backend Consistency Latency Operational cost When I pick it
Single Redis Weak under failover Very low, sub-ms Easy Best-effort locks where a rare double-grant is survivable
Redlock (5 Redis) Debated Low, single-digit ms Medium Rarely, see below
ZooKeeper / etcd Strong, consensus Higher, few ms Harder to run Correctness-critical locks, leader election
Database row As strong as the DB Depends on the DB Already there Low-traffic locks when you want zero new infra

The Redlock argument, which you should be ready to have.

  • The idea: run five independent Redis nodes and require a majority (three of five) to grant, so one node failing does not lose your lock.
  • It sounds safer.
  • The counterargument, the one I expect a strong candidate to raise: Redlock relies on timing assumptions distributed systems do not honor.
  • If a client pauses long enough, its lease expires regardless of how many nodes agreed, and you are back to two owners.
  • Going from 1 node to 5 does not fix the pause problem. Only fencing tokens fix the pause problem.
The lock protects efficiency
Single Redis lock
Goal
Avoid doing the same work twice.
A double-grant is
Annoying but harmless.
Mechanism
One SET NX PX, cheap and fast.
Fencing needed
No, a rare duplicate is survivable.
The lock protects correctness
Consensus store plus fencing
Goal
Never corrupt data or double-charge.
A double-grant is
A Sev 1 incident.
Mechanism
etcd/ZooKeeper lock plus tokens checked at the resource.
Fencing needed
Always, a lock alone is never enough.

The deeper trade-off: strong locks cost availability.

  • A consensus system needs a quorum.
  • A network partition that cuts off the quorum stalls acquisition.
  • That is CAP showing up in your lock: you are choosing consistency over availability.
  • Usually the right call for a lock, but say it out loud so the interviewer knows you made it on purpose.

5Where it is used#

Name real uses, because it shows you have seen locks in the wild, not just in a blog post.

Leader election is the biggest one.

  • A cluster of workers all try to grab one lock, and the winner becomes the leader that runs the cron job or the rebalancer.
  • Kafka's older controller election leaned on ZooKeeper for exactly this.
  • Kubernetes controllers use a lease object in etcd so only one instance reconciles at a time while the rest sit on standby.

Cron deduplication.

  • This is the billing example from section 3: a service on ten pods, but the nightly job must run once.
  • A lock keyed to the job name, with a TTL slightly longer than the runtime, means only one pod fires it.
  • Fencing is the real backstop.

Resource serialization in payments and inventory.

  • "Only one process may decrement the stock for SKU 12345 right now."
  • Honestly, here I would push back in an interview and suggest a conditional DB update or an atomic counter instead of a lock.
  • The database already gives you the atomicity for free.
  • Reaching for a distributed lock when a single UPDATE ... WHERE quantity > 0 would do is a small red flag.

The canonical references.

  • HDFS and Google's Chubby.
  • Chubby was built as a coarse-grained lock service and ended up used mostly for leader election and as a highly available config store.
  • That tells you something: people want locks less often than they want a reliable place to agree on one small fact.

6Common pitfalls and gotchas#

These are the ones that separate a candidate who read about locks from one who has run them.

The TTL guessing game.

  • Too short and your lease expires mid-job while you are still working.
  • Too long and a crashed holder blocks everyone for ages.
  • There is no perfect number, as the 120 second versus 35 second pause showed.
  • The mature answer: a lease you renew. A heartbeat extends the TTL every few seconds while you are alive, so a healthy long job keeps its lock but a dead one releases quickly.

Releasing a lock you no longer own.

  • Delete the key without checking your value and you can release someone else's lock.
  • Always do compare-value-then-delete atomically with a Lua script.

Assuming clocks agree.

  • Lock TTLs depend on time, and clocks drift between machines, often by tens of milliseconds and more after an NTP correction.
  • A client with a fast clock can think its lease is still good when the server already expired it.
  • Never reason about lock validity using your own wall clock alone.

Forgetting the pause.

  • GC pauses, container throttling, and VM migrations can freeze your process for seconds.
  • During that freeze the lock expired and someone else took it.
  • No fencing means your design is wrong for correctness-critical work.
  • Say "what if the process pauses right here" to yourself at every step.

Treating the lock as the source of truth.

  • The lock coordinates access, but the resource must still defend itself.
  • Fencing tokens live at the resource, not at the lock.
  • If you cannot change the resource to check tokens, you cannot build a truly safe lock around it, and you should be upfront about that limit.
Red flag that fails you here

Answering "what if that pod pauses for 40 seconds" with "I'll make the TTL longer." That just moves the cliff without removing it, and it tells me you think the lock is the thing keeping you correct. The lock picks a winner. Only the fencing token at the resource keeps a paused loser from writing.

7How it shows up in interviews#

Interviewers rarely ask "explain distributed locks" directly. It surfaces inside a larger design: a job scheduler, a payment system, a dedup layer. They wait to see whether you reach for a lock, and then whether you understand what you reached for.

A weak answer.

"I'll use a Redis lock with SET NX and a TTL, and whichever pod gets it runs the job."

  • That is the starting line, not the finish. It shows you know the command but not the failure model.
  • Ask "what if that pod pauses for 40 seconds" and the weak answer either goes quiet or says "make the TTL longer," which just moves the cliff.

A strong answer, same billing prompt.

"A single Redis SET NX PX lock picks which pod runs, that handles contention. But a lock alone cannot guarantee exactly-once charging, because a GC pause or VM migration can let the lease expire mid-job while the pod is frozen, and a second pod then starts. So the lock service hands out a monotonic fencing token, and the payment ledger rejects any charge carrying a token lower than the highest it has committed. The lock is an optimization to cut contention. The token at the ledger is what actually makes it correct. If this needs to survive a Redis failover cleanly, I would put the lock on etcd and accept that during a partition I lose the ability to acquire, because a double charge is far worse than a brief stall."

Why the strong answer wins (it is not vocabulary).

  • It names the two reasons to lock: efficiency versus correctness, and picks the mechanism to match.
  • It raises the pause problem and fencing before the interviewer pulls them out.
  • It questions whether a lock is even the right tool when a conditional update or idempotency key fits better.
  • It is honest about the availability cost.
  • Say all of that and I relax, because now I know you have been burned by this before.

The lock picks a winner. The fencing token keeps the losers from writing.

Follow-ups the interviewer will ask (and your answers)
"The lock holder's network dies right after acquiring but before releasing. Now what?"
The TTL expires and the lock frees itself, which is exactly why the TTL exists. No human has to intervene, and the next client can acquire once the lease ages out.
"How do you stop two holders from both writing to the resource?"
Fencing tokens checked at the resource, every time. The lock service issues a monotonically increasing token per grant, the resource records the highest it has committed, and it rejects anything lower. That is the ledger rejecting token 41 after it has already seen 42.
"Doesn't Redlock with five nodes solve the double-grant problem?"
No. Redlock survives a node failure, but it still relies on timing. If a client pauses long enough its lease expires no matter how many nodes agreed, and you are back to two owners. Going from 1 node to 5 does not address the pause. Only fencing tokens do.
"How do you pick the TTL for a job whose runtime varies?"
You do not pin a single number. You set a modest TTL and renew it with a heartbeat every few seconds while the holder is alive, so a healthy long job keeps its lease and a dead holder releases fast. The TTL is the crash safety net, not the correctness mechanism.
"When would you not use a distributed lock at all?"
When the resource already gives you atomicity. For "decrement stock for SKU 12345," a conditional `UPDATE ... WHERE quantity > 0` or an atomic counter is simpler and safer than a lock. An idempotency key also beats a lock for exactly-once semantics. Reaching for a lock when the database already serializes the write is a red flag.

What you can now explain in the room

  • Why a distributed lock is a time-boxed lease, not a guarantee, and why that makes the pause the central threat.
  • How `SET NX PX` acquires, why release needs an atomic compare-value-then-delete, and why a plain DEL is a bug.
  • Exactly how a paused holder causes split ownership, using the billing timeline and its ~15,000 duplicate charges.
  • Why a fencing token checked at the resource is the only thing that makes a lock correct under pauses.
  • When one Redis lock is fine (efficiency) versus when you need consensus plus fencing (correctness), and why Redlock does not change that.
  • Why a strong lock trades availability for consistency, and when a conditional update beats reaching for a lock at all.

For a full walkthrough, see the related System Design Question on designing a distributed job scheduler, where locking, leader election, and fencing all come together under real load.

Get a prep plan built around your Distributed Locks interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Distributed Locks prep plan is on the way.