Advanced13 min readLevel 2

Optimistic vs Pessimistic Locking

Two philosophies for when two people edit the same row at the same time.

Introduction

When two transactions touch the same data concurrently, one of them can silently overwrite the other — the classic 'lost update'. Locking is how you prevent it. The two families of strategy differ in when they assume conflict happens: pessimistic locking assumes conflict is likely and blocks upfront; optimistic locking assumes conflict is rare and only checks at commit time.

Why it exists

Imagine two support agents open the same ticket and both hit save. Without coordination, the second save clobbers the first, and the first agent's work vanishes with no error. Any system with concurrent writers to shared state — inventory, balances, documents, seat maps — needs a rule for who wins and how the loser finds out. Locking provides that rule.

Analogy

Pessimistic locking is checking out a library book: while you hold it, nobody else can. Safe, but others wait. Optimistic locking is editing a shared doc and only being warned at save time 'someone changed this since you opened it — reload'. No waiting, but you might have to redo work.

How it works

Pessimistic locking acquires a lock before reading/modifying (e.g. SELECT ... FOR UPDATE). Other transactions that want the same row block until the lock is released at commit. Correct and simple to reason about, but it holds locks — risking contention, reduced throughput, and deadlocks.

Optimistic locking takes no lock. It reads a version (a version number or timestamp) with the data, does its work, then on write asserts the version hasn't changed: UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?. If zero rows are updated, someone else won the race; the app retries with fresh data. No blocking, but conflicts cost a retry.

The choice hinges on conflict probability: high contention favors pessimistic (retries would thrash); low contention favors optimistic (locks would waste throughput).

Optimistic update lifecycle

  1. 1

    Read the row including its current version (say v7).

  2. 2

    Perform business logic in the application.

  3. 3

    Issue UPDATE ... WHERE id = ? AND version = 7, setting version = 8.

  4. 4

    If 1 row changed, success — you won the race.

  5. 5

    If 0 rows changed, someone committed v8 first: re-read and retry (with a cap).

Interactive

Architecture

Where each strategy pays its cost

Pessimistic

Acquire lockothers wait
Read + write
Release at commit

Optimistic

Read + version
Work (no lock)
Commit if version matcheselse retry

Pessimistic pays with waiting up front; optimistic pays with an occasional retry at the end.

In code

SQLPessimistic: SELECT ... FOR UPDATE
BEGIN;
-- Lock the row; concurrent FOR UPDATE on the same row blocks here.
SELECT balance FROM accounts WHERE id = 42 FOR UPDATE;

-- Safe to read-modify-write; no one else can touch row 42.
UPDATE accounts SET balance = balance - 100 WHERE id = 42;
COMMIT;  -- lock released
TypeScriptOptimistic: version check on write
async function transferWithRetry(id: number, delta: number) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const { balance, version } = await db.one(
      "SELECT balance, version FROM accounts WHERE id = $1",
      [id],
    )
    const next = balance + delta
    const res = await db.result(
      `UPDATE accounts SET balance = $1, version = version + 1
       WHERE id = $2 AND version = $3`,
      [next, id, version],
    )
    if (res.rowCount === 1) return // won the race
    // else someone updated concurrently — loop re-reads fresh data
  }
  throw new Error("Too much contention, giving up")
}

In the real world

Where you've seen this

ORMs bake this in: JPA/Hibernate's @Version, Rails' lock_version, and many document stores' conditional writes are optimistic locking. E-commerce inventory decrements often use optimistic checks ('decrement only if stock >= qty'); banking ledgers and seat reservations frequently use pessimistic locks or serialized queues because the cost of a lost update is severe and contention on a hot row is real.

When to use it

Reach for it when

  • Optimistic: conflicts are rare (most rows edited by one writer at a time).
  • Optimistic: you want maximum read throughput and short-lived writes.
  • Pessimistic: conflicts are frequent on the same rows (hot inventory, a popular seat).
  • Pessimistic: a retry is expensive or side-effecting and you'd rather serialize.

Avoid it when

  • Optimistic on a highly-contended hot row — retries thrash and starve writers.
  • Pessimistic across a user 'think time' (never hold a DB lock while waiting for a human).
  • Pessimistic when locks span services or long operations — use a queue or saga instead.

Trade-offs

Advantages

  • Optimistic: no blocking, high concurrency, no deadlocks, cheap when conflicts are rare.
  • Pessimistic: simple mental model, guarantees exclusivity, no wasted retry work under contention.

Disadvantages

  • Optimistic: wasted work + retries under contention; app must handle the conflict.
  • Pessimistic: reduced throughput, risk of deadlocks, and locks held too long stall the system.

It's fundamentally 'pay upfront with waiting' (pessimistic) vs 'pay at the end with occasional rework' (optimistic), and the right choice is dictated by conflict probability. Optimistic maximizes throughput when contention is low and degrades badly when it's high; pessimistic guarantees progress on hot rows at the cost of concurrency and deadlock risk. For extreme hot spots, sidestep both by serializing through a single-writer queue or using an atomic conditional operation.

Optimistic vs PessimisticOptimisticPessimistic
AssumesConflicts are rareConflicts are likely
Locks heldNoneUntil commit
Cost paidRetry on conflictWaiting / blocking
DeadlocksNoPossible
Best forLow-contention rowsHot, highly-contended rows

Common mistakes

Watch out for

  • Holding a pessimistic lock across user think-time (open edit form → lock → user goes to lunch).
  • Optimistic locking with unbounded retries, creating a thundering herd on a hot row.
  • Forgetting to increment the version, so the guard never detects conflicts.
  • Assuming a read-modify-write in app code is atomic — it isn't without one of these strategies.
  • Acquiring multiple pessimistic locks in inconsistent order, causing deadlocks.

Failure thinking

What breaks it

A flash sale has 10,000 users buying the last 100 units. You used optimistic locking on the stock row. What happens?

Almost every write fails the version check and retries, and the retries collide again — a thundering herd that wastes CPU and slows everyone. On a single hot row, pessimistic locking (or serializing through a queue / atomic decrement) is better because it removes the retry storm. Optimistic shines when conflicts are rare, which a flash sale is the opposite of.

Two transactions each lock row A then row B, in opposite order. What happens?

Deadlock: each holds what the other needs. The database detects it and kills one transaction (which you must catch and retry). Prevent it by always acquiring locks in a consistent global order, keeping transactions short, and using timeouts. Deadlocks are a pessimistic-locking hazard optimistic locking doesn't have.

Think like a senior

Senior Engineer Insight

The senior move is to reason about conflict probability per row, not per system. The same app might use optimistic locking for user profile edits (rarely concurrent) and a serialized queue or atomic decrement for a hot inventory counter (highly concurrent).

Senior Engineer Insight

Never hold a lock across anything you don't control the duration of — especially a human or a remote call. If you feel tempted to, you actually want optimistic concurrency or a workflow/saga.

Remember

Optimistic = detect conflict at commit and retry; pessimistic = prevent conflict by blocking.

Remember

High contention → pessimistic (or serialize). Low contention → optimistic.

Interview questions

1

Explain optimistic vs pessimistic locking and how you'd choose.

2

How does optimistic locking actually detect a conflict?

Active recall

Check yourself

Optimistic vs Pessimistic Locking · Question 1 / 1Hard

500 users try to buy the last concert seat at the same time. You expect very high contention on that row. Which approach avoids wasted retries?

Practical challenges

Medium45–90 min

Implement optimistic concurrency

Add version-based optimistic locking to a simple key-value store with a compare-and-set update.

You have an in-memory map of id → { value, version }. Simulate two concurrent updaters.

Requirements

  • read(id) returns value and version.
  • update(id, newValue, expectedVersion) succeeds only if the stored version matches, then increments it.
  • A losing update returns a conflict result the caller can retry.

Acceptance criteria

  • Two updates from the same base version — only one succeeds.
  • The loser can re-read and retry successfully.
  • Version increments by exactly one per successful write.

Edge cases

  • Updating a non-existent key.
  • expectedVersion far behind current.

Bonus

  • Add a retry helper with a bounded attempt count and jittered backoff.

Reflection

  • What breaks if you forget to increment the version?
Architecture1–3 hours

Kill the flash-sale retry storm

Redesign a hot inventory decrement that's collapsing under optimistic-lock retries during a flash sale.

One SKU, 100 units, 50k concurrent buyers. The current code does read-version → decrement → CAS, and retries are melting the DB.

Requirements

  • Explain why optimistic locking fails here.
  • Propose at least two contention-removing designs.
  • Guarantee no oversell and no double-charge.

Acceptance criteria

  • Chosen design bounds work-per-request regardless of concurrency.
  • Exactly 100 units sell, never 101.
  • Losers get a clear 'sold out' fast, not a timeout.

Bonus

  • Show how idempotency keys prevent a retry from double-decrementing.

Reflection

  • Where would you push back if a PM asks to 'just increase the retry limit'?

Architecture challenge

Design concurrency control for an event-ticketing system selling a 20,000-seat stadium. Popular seats get thousands of simultaneous buyers; obscure seats get one. Decide per-access-pattern: how do you handle a single hot seat, a whole section going on sale at once, and the cart-hold window while a user enters payment? Justify where you use optimistic, pessimistic, or a queue, and how you avoid both lost sales and double-selling a seat.

Flashcards

Flashcards1 / 4

Summary

Optimistic and pessimistic locking both prevent lost updates but bet differently on how often conflicts occur. Pessimistic blocks other writers upfront (safe, but reduces concurrency and risks deadlocks); optimistic checks a version at commit and retries on conflict (high throughput when conflicts are rare, degrades under contention). Match the strategy to per-row conflict probability, and remove contention entirely for extreme hot spots.

Key takeaways

  • Pessimistic prevents conflicts by waiting; optimistic detects them at commit and retries.
  • Low contention → optimistic; high contention → pessimistic or serialize.
  • A version column (check-and-set) is the whole optimistic mechanism.
  • For hot rows, remove contention: atomic conditional writes, counters, or single-writer queues.

Your notes

Saved to this device