Immutability & Concurrency Control

Optimistic vs Pessimistic

Four numbers decide it: how often two writers collide, how expensive a conflict is to resolve, how long the state must stay stable, and what a retry costs. Optimistic wins comfortably at low contention and degrades badly — not gradually — as contention rises. That curve is the lesson.

▶ Run the lab

The question this answers

The question

At what contention level does letting writers race and retry become worse than making them queue?

The work

The same conditional update, applied to two rows: a customer profile edited twice a month, and a global signup counter incremented 400 times a second.

What is shared

One row per case. What differs is not the mechanism but the arrival rate against that row — which is why the same code is correct in both places and appropriate in only one.

The invariant — what must stay true under every interleaving

Every committed write is derived from the state that was current when its decision was made, and no committed write is silently overwritten — under either strategy.

Synchronization exists to preserve this sentence. If a schedule can make it false, the code is wrong no matter which primitive it uses.

WorkCan it overlap?Can it parallelise?What is shared?What ordering?What synchronization?Where is contention?What can deadlock?What can race?What is gained?What complexity?

The four inputs

The decision is not philosophical. It is a comparison of expected costs, and four quantities determine it: the probability that two writers overlap on the same key, the cost of resolving a conflict once detected, the length of time the state must remain stable for the operation to be correct, and the cost of one discarded attempt.

Optimistic is right when overlap is unlikely, the think time is long, and a wasted attempt is cheap. Pessimistic is right when overlap is likely, holding the state is cheap because the critical section is short, or a conflict cannot be resolved by retrying at all because a side effect already happened.

The trap is applying one answer globally. Almost every real system needs both, keyed by the row: optimistic on the 40 million entity rows, pessimistic — or a commutative atomic update, which is better than either — on the four rows everybody hits. A per-key conflict rate is what tells you which is which.

DimensionOptimisticPessimisticWhich wins
Contention on the keyCosts nothing when writers rarely overlapPays coordination on every operation regardlessOptimistic below a few percent conflict rate
High contention on one keyRetries multiply; throughput collapsesQueue forms; throughput is stable but serializedPessimistic — or remove the shared key entirely
Think time between read and writeUnbounded — holds nothingLock held the whole time, blocking everyoneOptimistic for human or multi-second think time
Critical section lengthIrrelevant — no section is heldThe entire cost modelPessimistic only when the section is genuinely short
Cost of one wasted attemptPaid in full by every loserNever paid — losers wait, they do not computePessimistic when the computation is expensive
Conflict resolvable by retry?Requires it; blind retry can corrupt intentNo conflict to resolvePessimistic when the operation is not safely repeatable
Side effects before commitDangerous — an emailed attempt is not un-emailedSafe, the decision happens oncePessimistic, or move side effects after commit
Crossing a network / process boundaryNatural — no ownership to loseNeeds leases, fencing and crash recoveryOptimistic
Latency predictabilityDepends on how many others showed upBounded by queue length times hold timePessimistic
Observability of the costAttempts per commit, conflict rateWait time, hold time, waiter countBoth are measurable — measure before choosing
The decision, dimension by dimension.

The curve: graceful until it is not

Under a lock, adding writers lengthens a queue. Commit throughput stays roughly flat at one critical section per unit time and latency grows linearly with the queue. That is unexciting and entirely predictable, which is its own kind of virtue.

Under optimism, adding writers does something qualitatively different. With w concurrent writers on one key, roughly one attempt in w succeeds, so the useful fraction of the work falls as writers rise while the total work rises with them. Effective throughput does not plateau — it turns over and comes down, because the retries are load. Every additional writer both consumes capacity and lowers everyone else's success probability.

That is why the failure feels sudden in production. A key sits at a 2% conflict rate for a year, a feature launch triples write traffic to it, and the conflict rate goes to 60% rather than 6% — throughput drops, latency rises, and the system is now spending most of its database budget on updates it discards. The correct response is not more retries or longer backoff; it is to change the shape of the key.

Modelled useful commit throughput against concurrent writers on ONE hot key, normalized to a single writer.SIMULATED
1 workerdashed = linear speedup32 workers · max 32.0×
Modelled, not measured: it assumes one contended key, uniform attempt cost and a bounded-jitter retry policy. The shape is the teaching point — a lock-based curve on the same axes would flatten near 1.0 and stay there instead of turning over. Optimistic control has no plateau to degrade onto.

The third answer, which is usually the right one

The question "optimistic or pessimistic" presumes the contention is inherent. Very often it is an artifact of the data model, and the best available move is to stop having a contended key at all.

Three transformations do most of the work in practice. Make the operation commutative, so the outcome does not depend on what was read — SET count = count + 1 needs neither a version nor a lock, because the store applies it atomically and order does not matter. Shard the hot key into N counters summed on read, converting a 60% conflict rate into a 60/N one. Or serialize per entity through a queue or actor, so writes to one key are ordered by construction and both strategies become moot.

When contention genuinely is inherent — a seat count that must never oversell — pessimistic locking on a short critical section is the honest answer, and the conditional single-statement update (WHERE left > 0) is usually better than both, because it is one indivisible operation with no lock held and no retry loop.

Concurrent increments on one counter, four ways — the naive one first, so the baseline is a failure.SIMULATED
Invariant · The counter equals the number of completed increments.
#Writer 1Writer 2Writer 3State
1NEITHER: read count=100··count=100
2·read count=100·count=100
3write count=101··count=101 increments=1
4·write count=101·count=101 increments=2
✕ Two increments completed and the counter advanced by one. No lock, no version, no error — the lost update the other three strategies exist to prevent.
5OPTIMISTIC: read count=100 (v9)··count=100 version=9
6·read count=100 (v9)·count=100 version=9
7··read count=100 (v9)count=100 version=9
8CAS v9 -> 101/v10 OK··count=101 version=10
9·CAS v9 -> 101/v10 FAIL, re-read, retry·count=101 version=10 wasted=1
10··CAS v9 -> 101/v10 FAIL, re-read, retrycount=101 version=10 wasted=2
11PESSIMISTIC: lock, read 100, write 101, unlock··count=101 waiters=2
12·was blocked; acquires, reads 101, writes 102, unlock·count=102 waiters=1
13COMMUTATIVE: UPDATE t SET count = count + 1 (each writer, once)··count=103
The unguarded version loses an update silently; the other three all preserve the invariant and differ only in what they charge. Optimistic pays in discarded work that grows with contention, pessimistic pays in queueing that does not, and the commutative form pays almost nothing — because it never made a decision that could become stale.

Key points

  • The decision rests on four numbers: contention rate, conflict cost, how long the state must stay stable, and the cost of a discarded attempt.
  • Optimistic wins decisively at low contention, where it costs literally nothing and holds nothing.
  • Optimistic degrades non-gracefully: as writers on one key rise, useful throughput turns over and falls, because retries are load.
  • Pessimistic is boring in the good way — throughput plateaus and latency grows linearly, which is predictable and measurable.
  • The choice is per key, never per system, and the best answer is often neither: make the operation commutative, shard the key, or serialize per entity.

The loop, answered

Every field is required, which is why no lesson here can recommend concurrency without naming the interleaving that breaks it, the complexity it adds, and the simpler thing to consider first.

How it works
  • Measure the per-key conflict rate and attempts per successful commit; do not reason about contention from architecture diagrams.
  • Estimate the cost of one discarded attempt — the read, the computation and the failed write.
  • Estimate the critical section length a lock would need to hold, including anything that cannot be moved out of it.
  • If conflicts are rare and think time is long, take the optimistic path with a bounded jittered retry loop.
  • If conflicts are common and the section is short, take the lock — but first check whether the operation can be made commutative or the key sharded, which beats both.
  • Re-measure after traffic changes. A key that was optimistic-appropriate at 40 writes/sec may not be at 400.
Interleavings that matter
  • Low contention, optimistic: A reads v5, commits v6; B reads v6, commits v7 — zero conflicts, zero coordination, nothing held.
  • Low contention, pessimistic: A locks, commits, releases; B locks, commits, releases — same outcome, plus a lock acquisition per operation that prevented nothing.
  • High contention, optimistic: thirty writers read v9; one commits; twenty-nine fail and retry; on the next round one commits and twenty-eight fail — commit rate is one per round trip and CPU scales with writers.
  • High contention, pessimistic: thirty writers queue; each acquires in turn — commit rate is one per critical section, no work is discarded, and latency is position in queue times hold time.
  • Long think time, pessimistic: a human holds a row lock for four minutes with the form open, then closes the tab — every other editor of that customer is blocked until the lease expires.
  • Commutative: thirty writers each issue one count = count + 1; the store serializes them internally, all thirty succeed, none of them read anything and none of them can be stale.
What it guarantees — and does not
  • Both strategies guarantee the same invariant when applied correctly; they differ only in how the cost of contention is paid.
  • Optimistic guarantees no waiting and no wasted waiting; it does NOT guarantee progress or bounded latency under contention.
  • Pessimistic guarantees no wasted work and bounded per-operation latency at a given queue depth; it does NOT guarantee deadlock freedom or fairness.
  • Neither guarantees anything across multiple keys unless they share a transaction or an ordering discipline.
  • Neither is "the fast one". Which is faster is entirely a function of the conflict rate on that key at that moment.
  • A commutative atomic update guarantees correctness with neither retries nor a held lock — but only for operations that genuinely commute.
Where contention appears
  • Optimistic converts contention into wasted CPU and I/O; pessimistic converts it into wait time. Both are visible, in different metrics.
  • Optimistic contention is self-amplifying — retries add load which increases conflicts which adds retries. Lock contention is self-limiting: a longer queue does not make the critical section slower.
  • Under a lock, one slow holder hurts everyone equally and visibly. Under optimism, one slow writer mostly hurts itself, by never winning.
  • Both concentrate on hot keys, and both look healthy in aggregate metrics while one key is pathological.
How it fails
  • Optimistic: retry storm, starvation of slow writers, wasted-work amplification, semantic corruption from blind retries, side effects performed on discarded attempts.
  • Pessimistic: deadlock from inconsistent lock ordering, convoys, priority inversion, pool exhaustion by blocked workers, orphaned locks when a holder dies.
  • Both: applying one strategy uniformly and discovering the hot key only during an incident.
  • Both: measuring conflict or wait rates in aggregate, which hides the single key responsible for the problem.
  • Switching from optimistic to pessimistic under load without shortening the critical section, which trades a retry storm for a convoy.
When it helps
  • Optimistic helps for per-entity edits, human-scale think time, cross-network updates, and read-heavy systems where writer locks would hurt readers.
  • Pessimistic helps for hot single keys, short critical sections, non-repeatable operations, and anywhere predictable latency beats peak throughput.
  • Knowing both helps most: the ability to say "this key is optimistic and that one is locked" is what separates a working design from a global policy.
  • Measuring first helps more than either — the conflict rate is cheap to collect and settles the argument.
When it hurts
  • Choosing optimistically because it sounds modern, on a key that forty writers touch every second.
  • Choosing pessimistically because it sounds safe, then holding the lock across a network call.
  • Applying either uniformly across a schema whose keys have conflict rates spanning four orders of magnitude.
  • Tuning the retry policy when the real answer is that the key should not be shared at all.
How you would know
  • Attempts per successful commit, per key — the single most decisive number, and the one that tells you when to switch.
  • Lock wait time and hold time distributions, per key, for the pessimistic side of the comparison.
  • Total work per successful commit under each strategy: the cost of every attempt, not just the winning one.
  • Commit throughput against concurrent writers, sampled at real traffic levels rather than modelled — the curve turning over is the signal to change the key's shape.
  • Per-key write arrival rate, tracked over time, because the strategy that was right last quarter may not be right after a launch.
Complexity it introduces
  • Optimistic adds a conflict path, a retry policy, a version token threaded through every layer, and a rule about where side effects may live.
  • Pessimistic adds lock ordering as a global codebase property, release-on-exception discipline, timeout tuning and non-composable functions.
  • Running both adds the obligation to know which keys use which — a fact that lives in someone's head unless it is written into the code and the runbook.
  • Changing strategy later is a migration, not a config flag, because the two have different failure modes and different metrics to watch.
Simpler alternatives
  • A commutative atomic update, which needs neither strategy: the store applies it indivisibly and the outcome does not depend on what anybody read.
  • Sharding the hot key into N sub-keys summed on read, which reduces conflicts and queueing simultaneously.
  • Serializing per entity through a queue or actor so ordering is structural rather than enforced. See The Actor Model and Message Passing.
  • Pushing the decision to the database's isolation level and letting it detect conflicts. See Isolation Levels.
  • Removing the shared state — the reliably strongest move available. See Immutability as a Concurrency Strategy.

Optimistic concurrency lab

Optimistic vs pessimistic under contention
A version-checked update: read the row with its version, compute, write back only if the version is unchanged, retry if it moved.
SIMULATEDThe shape of the curve is the lesson; the axis numbers are not.

Conflict probability grows with the number of writers touching the same item, each conflict costs a full retry, and retries consume the same cores the successful work needs. Real systems add their own effects — backoff, hot keys, transaction size — but the turnover is real and it is why an optimistic scheme that benchmarked beautifully at low load can collapse at high load rather than merely slow down.

optimistic (version check + retry)pessimistic (lock, then work)x: 1–48 · y max 730 commits/s
optimistic
646/s
pessimistic
196/s
attempts per commit
1.24
work thrown away
19%
retry rate (attempts that fail the version check)19.2%
commits landing646.4/s
At 8 writers and 3% contention the optimistic scheme commits 646/s against the lock’s 196/s: 19% of attempts retry, which is cheap enough that never waiting wins. Note where it peaks — 730/s at 4 writers — and that past that point adding writers makes the system slower, not faster.
Crossover at 48 writers: below it, waiting is the waste; above it, retrying is. Neither scheme is “the fast one” — the contention rate decides, and the contention rate is a property of your data, not of your code.
Both schemes preserve the same invariant — no lost update — and both are correct. What differs is where the cost lands: predictable waiting versus unpredictable wasted work, and a retry loop that must be bounded or it becomes a livelock. The version check itself is the contract; API Design and Database Engineering cover how it is exposed and how a store implements it.
optimistic leads at 8 writersSIMULATED

Eight threads, one lock

Eight threads, one lock
Every thread does some work, then takes the same mutex. Watch how much of each lane is spent waiting for a turn, and what the machine actually delivers.
8 cores
Thread 1
work
lock
work
wait
lock
work
Thread 2
work
wait
lock
work
wait
lock
work
Thread 3
work
wait
lock
work
wait
lock
Thread 4
work
wait
lock
work
wait
Thread 5
work
wait
lock
work
wait
Thread 6
work
wait
lock
work
wait
Thread 7
work
wait
lock
work
wait
Thread 8
work
wait
lock
work
wait
runningreadywaitingblockedidle24 ms of wall clock
throughput
500/s
effective parallelism
2.50 / 8
lock busy
90.0%
mean lock wait
18 ms
serialised share of each task0.4 · 2.0 ms locked of 5.0 ms total — 40.0%
The critical section is busy 90% of the time. It is now the ceiling: more cores and more workers change nothing. Effective parallelism is 2.5 on 8 cores — the definition of false parallelism. Shrink the critical section or shard the lock. The critical section is 40.0% of each task, so 2.5 of 8 cores' worth of work is really happening at once. Waiting is not evenly distributed either: mean lock wait is 18 ms, and the tail is far worse than the mean because queueing delay grows non-linearly as the lock approaches saturation. Contention is not caused by threads; it is caused by the fraction of the work that must be serialised. Adding threads to a contended lock adds queue, not capacity — and past that point each extra thread makes the tail latency worse while leaving throughput exactly where it was.
SIMULATEDLanes are a discrete simulation of one mutex granted in arrival order; throughput comes from the lab model. Neither is a measurement, and real locks add cache-line traffic this omits.

What people believe, and what is true

Claim

Optimistic concurrency scales better.

Reality

It scales better where conflicts are rare and worse where they are not — and it degrades by turning over rather than plateauing, so the failure arrives all at once rather than as a gradual slowdown you can react to.

Claim

You pick one strategy for the system.

Reality

You pick per key. A schema commonly wants optimistic control on entity rows and something else entirely on the handful of rows every request touches.

Claim

If the conflict rate is high, add more retries or longer backoff.

Reality

That reduces the storm without fixing the cause and multiplies latency. A high conflict rate is a signal about the shape of the key: shard it, make the operation commutative, or serialize it.

Go deeper

Overview

If collisions are rare, let writers race and detect the loser. If collisions are common, make them queue. Which one applies is a property of the key, not of your taste.

Practical

Instrument attempts-per-commit per key. Use optimistic control by default for entity edits; switch the handful of hot keys to a lock, a commutative update, or a shard.

Advanced

Model the total work per commit under each strategy at your actual writer concurrency. Optimistic total work grows with contenders while useful output falls; lock-based work stays flat while latency grows. That asymmetry is the whole decision.

Internals

Both strategies exist at every level of the stack with the same shape and wildly different attempt costs — a CAS retry loop on a cache line costs nanoseconds, an HTTP 412 retry costs a round trip. The acceptable conflict rate therefore differs by orders of magnitude depending on where the loop lives.

Apply it