Database concurrency comparison
Optimistic vs Pessimistic Locking
Two strategies for handling concurrent writes to the same database row. The choice affects throughput, deadlock risk, and complexity — and it comes up frequently in backend code reviews and system design interviews.
TL;DR
- Optimistic locking — no lock held while you work. On commit, check if the version field matches; if not, someone else got there first — retry. Great for low-conflict scenarios. No deadlocks.
- Pessimistic locking — lock the row immediately on read (SELECT FOR UPDATE). Nobody else can modify it until you release. Guarantees safety but risks deadlocks and reduces concurrency.
- Default: optimistic. Use pessimistic only when conflicts are truly frequent or when a retry is unacceptable (financial transactions, limited inventory).
Side-by-side comparison
| Aspect | Optimistic Locking | Pessimistic Locking |
|---|---|---|
| Lock held | No — check on commit | Yes — row locked on read |
| Throughput | High when conflicts rare | Lower — readers queue behind writer |
| Deadlock risk | None | Possible — lock ordering matters |
| Conflict handling | Retry or surface error to user | Blocked until lock released |
| Implementation | Version column + conditional UPDATE | SELECT ... FOR UPDATE in transaction |
| Best conflict rate | Low — few writers on same row | High — many writers on same row |
| Use case | User profile edits, CMS articles, e-commerce catalogue | Bank transfers, seat/ticket booking, inventory deduction |
| ORM support | @Version (JPA), lock_version (Rails), @VersionColumn (TypeORM) | with_for_update() (SQLAlchemy), .lock() (Rails), SKIP LOCKED |
Code side-by-side
Updating a product stock count safely:
Optimistic locking (SQL)
-- Read the row and remember version
SELECT id, stock, version
FROM products WHERE id = 7;
-- → { stock: 10, version: 3 }
-- Business logic: reserve 1 item
-- Write back only if version matches
UPDATE products
SET stock = 9, version = 4
WHERE id = 7 AND version = 3;
-- Affected rows = 0? → retry!
-- Affected rows = 1? → success Pessimistic locking (SQL)
BEGIN;
-- Lock the row immediately
SELECT id, stock
FROM products
WHERE id = 7
FOR UPDATE;
-- Other transactions block here ↑
-- Business logic: reserve 1 item
UPDATE products
SET stock = stock - 1
WHERE id = 7;
COMMIT; -- Lock released When to use Optimistic Locking
- User-facing edit forms. Two users editing their own profiles rarely collide. Version check catches the rare case without blocking everyone.
- CMS and document editing. Conflicts are uncommon; a "someone else saved this document" message is acceptable.
- High-read, low-write data. Product catalogue, configuration records — reads vastly outnumber writes, so holding locks would be wasteful.
- Microservices across network boundaries. You cannot hold a database lock across an HTTP call; optimistic locking with retry is the right model.
When to use Pessimistic Locking
- Last-item inventory. When only one seat or ticket remains, optimistic retries from many concurrent buyers will all fail except one — pessimistic locking queues them cleanly.
- Financial debit/credit. Retrying a payment deduction risks double-charging if not carefully idempotent. Pessimistic locking prevents the race entirely.
- Short, fast transactions. Pessimistic locking is safer when the critical section is tiny (milliseconds) — the lock is held briefly and the queuing cost is low.
- Background jobs consuming a queue. SKIP LOCKED in Postgres lets workers grab the next unclaimed job row without contention.
English phrases engineers use
Optimistic locking conversations
- "We use a version column to detect concurrent edits."
- "If the UPDATE affects zero rows, we got a conflict — retry."
- "This is an optimistic check-and-set pattern."
- "The ORM throws an OptimisticLockException — catch and retry."
- "Conflicts are rare here — optimistic is the right trade-off."
Pessimistic locking conversations
- "We SELECT FOR UPDATE to lock the row before reading the balance."
- "The second transaction is blocked waiting for the first to commit."
- "We got a deadlock — transactions were locking rows in different orders."
- "Use SKIP LOCKED so workers don't queue on the same job row."
- "Keep the transaction short — holding a lock for too long causes queuing."
Quick decision tree
- Low conflict rate, user-facing forms → Optimistic
- High conflict rate, limited resource (last ticket) → Pessimistic
- Cannot afford a retry (payment) → Pessimistic
- Long business logic between read and write → Optimistic (never hold a lock that long)
- Background job queue (claim next item) → Pessimistic + SKIP LOCKED
- Microservice calling external API between read and write → Optimistic
- Many concurrent readers, rare writes → Optimistic
Frequently asked questions
What is optimistic locking?
Optimistic locking assumes conflicts are rare. You read a row, remember its version number (or timestamp), do your business logic, then write back only if the version has not changed. If it has changed, another transaction beat you — you retry or surface an error. No lock is held while you think.
What is pessimistic locking?
Pessimistic locking assumes conflicts are likely. You lock the row (SELECT ... FOR UPDATE) the moment you read it, blocking any other transaction from modifying it until you commit or roll back. Safe but can cause queuing and deadlocks under load.
What is a version field and how does it work?
A version field (often called version or updated_at) is a column you increment on every update. When you read a row you note the current version. When you write, your UPDATE includes WHERE version = <what you read>. If 0 rows are affected, someone else changed it first — you lost the race and must retry.
Which strategy has higher throughput?
Optimistic locking typically has higher throughput when conflicts are rare, because threads never block waiting for a lock. However, if the conflict rate is high (many writers on the same row), you waste CPU on retries and throughput can drop below pessimistic locking.
Can optimistic locking lead to deadlocks?
No. Optimistic locking never holds locks, so deadlock is impossible. Pessimistic locking can deadlock when two transactions lock rows in different orders and each waits for the other.
Which ORMs support optimistic locking out of the box?
Most major ORMs do. Hibernate/JPA (@Version annotation), ActiveRecord (lock_version column), Django (no built-in, but easy to implement with F expressions), TypeORM (@VersionColumn decorator), and SQLAlchemy (version_id_col). Pessimistic locking is usually exposed via with_for_update() or similar query methods.