The question this answers
At what contention level does letting writers race and retry become worse than making them queue?
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.
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.
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.
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.
| Dimension | Optimistic | Pessimistic | Which wins |
|---|---|---|---|
| Contention on the key | Costs nothing when writers rarely overlap | Pays coordination on every operation regardless | Optimistic below a few percent conflict rate |
| High contention on one key | Retries multiply; throughput collapses | Queue forms; throughput is stable but serialized | Pessimistic — or remove the shared key entirely |
| Think time between read and write | Unbounded — holds nothing | Lock held the whole time, blocking everyone | Optimistic for human or multi-second think time |
| Critical section length | Irrelevant — no section is held | The entire cost model | Pessimistic only when the section is genuinely short |
| Cost of one wasted attempt | Paid in full by every loser | Never paid — losers wait, they do not compute | Pessimistic when the computation is expensive |
| Conflict resolvable by retry? | Requires it; blind retry can corrupt intent | No conflict to resolve | Pessimistic when the operation is not safely repeatable |
| Side effects before commit | Dangerous — an emailed attempt is not un-emailed | Safe, the decision happens once | Pessimistic, or move side effects after commit |
| Crossing a network / process boundary | Natural — no ownership to lose | Needs leases, fencing and crash recovery | Optimistic |
| Latency predictability | Depends on how many others showed up | Bounded by queue length times hold time | Pessimistic |
| Observability of the cost | Attempts per commit, conflict rate | Wait time, hold time, waiter count | Both are measurable — measure before choosing |
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.
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.
| # | Writer 1 | Writer 2 | Writer 3 | State |
|---|---|---|---|---|
| 1 | NEITHER: read count=100 | · | · | count=100 |
| 2 | · | read count=100 | · | count=100 |
| 3 | write 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. |
| 5 | OPTIMISTIC: 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 |
| 8 | CAS 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, retry | count=101 version=10 wasted=2 |
| 11 | PESSIMISTIC: lock, read 100, write 101, unlock | · | · | count=101 waiters=2 |
| 12 | · | was blocked; acquires, reads 101, writes 102, unlock | · | count=102 waiters=1 |
| 13 | COMMUTATIVE: UPDATE t SET count = count + 1 (each writer, once) | · | · | count=103 |
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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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
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.
Eight threads, one lock
What people believe, and what is true
Optimistic concurrency scales better.
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.
You pick one strategy for the system.
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.
If the conflict rate is high, add more retries or longer backoff.
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.