Module 07 · Lesson 02

Leases and locks, and why a lock is a promise about time

A lock without an expiry is a permanent outage waiting for a single crash.

Advanced 7 min read Updated 2026-11-02

What you will be able to do

  • Explain a lease and the failure mode it introduces
  • Use fencing tokens to stop an expired holder from writing
  • Choose between pessimistic locking and conditional writes
  • Recognise when a distributed lock is unnecessary

The expired holder

Worker A takes a lease for ten seconds. It then stalls — a long garbage-collection pause, a frozen VM, a saturated network. The lease expires and worker B takes it. Worker A wakes up believing it still owns the work and writes. Now two writers acted on the same resource with full confidence.

This is not a rare bug
Pauses of several seconds happen in ordinary production: stop-the-world collections, live migrations, overloaded hypervisors. If your lease is shorter than your worst pause, you will eventually have two owners.

The standard fix is a fencing token: a monotonically increasing number issued with each lease. Every write to the protected resource includes the token, and the resource rejects any token lower than the highest it has seen. Worker A's write arrives with token 41 after B has written with 42, and it is refused.

A: acquire lock("job-7") -> token 41, lease 10s
A: GC pause 14s
B: acquire lock("job-7") -> token 42
B: write(resource, token=42)   accepted
A: write(resource, token=41)   REJECTED, 41 < 42

The cheaper alternative: conditional writes

ApproachHow it worksBest for
Conditional updateUPDATE ... WHERE version = 7Single-row state transitions
Unique constraintInsert fails if the key existsDeduplication, one-time claims
Compare-and-setSet value if current value matchesCounters, config flips
Lease + fencingToken older writes are rejectedLong jobs with external side effects
Partition ownershipOne consumer owns a key rangeStream processing

The first three need no coordination service. A database with a version column can serialize state changes for a single row, and the row is the lock. In a design, reaching for Redis or ZooKeeper before checking whether a conditional write suffices reads as inexperience.

Lock scope and ordering

  • Hold for milliseconds. A lock held across an HTTP call to a third party is held for an unbounded time.
  • Acquire in a fixed order. Two workflows locking A then B and B then A is a deadlock by construction.
  • Always set a TTL. A lock with no expiry survives exactly until the first crash.
  • Do not queue behind a lock. Reject with retry-after instead of building a queue of waiting requests that will time out together.

Scheduled jobs, the classic case

A nightly job runs on five instances; you want one to actually run it. Options in increasing cost: let every instance try a conditional insert of a job-run row keyed by date (cheap, correct for one execution), use the scheduler's built-in single-flight, or hold a lease with a fencing token. The conditional insert is usually enough and needs no new infrastructure.

Lease duration
5-30 s
Longer than p99 pause, shorter than tolerable stall
Renewal interval
1/3 of lease
Two missed renewals means lost ownership
etcd write latency
1-5 ms
Same region; it is a consensus write
Lock contention alert
> 5% of requests
Signals a design that will not scale
The answer that shows judgement
"I'd avoid a distributed lock. The state transition is a conditional update on a version column, which serializes it without extra infrastructure. If I needed single-flight for a scheduled job, I'd use a short lease with a fencing token so a paused owner cannot write after expiry."

Worth remembering

  • A lease trades correctness for liveness: the holder may still be alive after its lease expires.
  • Fencing tokens make storage reject writes from a stale holder, which is the only real fix.
  • Conditional writes or compare-and-set solve most lock needs without a lock service.
  • Hold locks for milliseconds, never across a user request or a network call you don't control.

Checkpoint

Answer all 3 questions to finish the lesson.
1. A worker's lease expires during a long pause, then it resumes and writes. What prevents corruption?
The expired owner cannot be stopped by time alone, so storage must reject its write. Fencing tokens make staleness visible to the resource.
2. Two services update the same order row. Which is the simplest correct coordination?
Optimistic concurrency gives serialization for one row with no new infrastructure. A lock service adds a failure mode for a problem the database already solves.
3. Why is a lock with no expiry dangerous?
Without a TTL, recovery requires manual intervention. The lock outlives the process that was supposed to release it.