Hook: Interviewers love this topic because it reveals whether you know how to stop two transactions from grabbing the same data at the same time.
Question: What is pessimistic locking in SQL?
Answer: Pessimistic locking is a technique where the database locks a row before another session can change it. It is used when conflicts are likely and correctness matters more than maximum concurrency. In SQL, you usually see it with row locks such as SELECT ... FOR UPDATE.
Interview-Ready Answer: I use pessimistic locking when I expect two users or services to touch the same row and I want the database to serialize that access. The first transaction locks the row, and the others wait, fail fast, or skip it depending on the SQL option I choose. A common example is inventory or seat reservation, where I lock the row before decrementing stock so the last item cannot be sold twice.
Detailed Explanation: “Pessimistic” means the database assumes conflicts are likely. Instead of letting two sessions read the same data and hoping they do not clash, it reserves the row first. A row lock is a short-term claim on a specific row; other transactions cannot update it until the lock is released at commit or rollback.
SELECT ... FOR UPDATE on one or more rows.NOWAIT, or skip locked rows with SKIP LOCKED if the database supports it.COMMIT or ROLLBACK.Some engines also lock extra key ranges, not just the exact row, to prevent phantoms (new rows appearing in a range you already checked). For example, InnoDB may use next-key locking in range queries under repeatable-read behavior. That is why a query on a range can block more than one row.
SKIP LOCKED to safely claim work.Use it when conflicts are common and retries would be expensive or user-visible. If conflicts are rare, pessimistic locking can reduce throughput because sessions spend time waiting instead of doing useful work.
| Topic | Pessimistic | Optimistic |
|---|---|---|
| Main idea | Lock first | Check later |
| Best for | Hot rows | Rare conflicts |
| Failure mode | Waiting | Retry |
| App logic | Simpler | More checks |
| Throughput | Lower under contention | Higher when calm |
Why the choice matters: pessimistic locking spends work up front to prevent conflicts. Optimistic locking lets work proceed and only checks for a conflict at write time, often using a version column. If ten users rarely touch the same row, optimistic locking usually scales better. If ten users often race for the same row, pessimistic locking avoids repeated failures and retries.
Acquiring a row lock is usually fast, roughly O(1) per locked row from the app’s point of view, but contention changes the picture. The database must track the lock in memory, and waiting transactions create a queue. In real systems, if 30 requests line up behind one row and each transaction holds the lock for 20 ms, the last request may wait about 600 ms before it can even start its update.
Timeout behavior is database-specific. PostgreSQL waits until the lock is released unless you set lock_timeout or use NOWAIT. MySQL/InnoDB commonly uses a finite lock-wait timeout such as 50 seconds by default. SQL Server can also be configured with a lock timeout; without it, sessions may wait a long time. That is why interviewers care about both correctness and user latency.
SELECT is not enough: a normal read usually does not protect the row from later updates; you need the locking form.Memory mental model: think of pessimistic locking as putting your hand on the last donut before asking who wants it. You are not being rude; you are preventing a fight over the same donut.
Real-World Example: Imagine a ticketing checkout service for a concert. Each checkout request must reserve the last available seat in a specific row. The service starts a transaction, locks the seat record, checks availability, writes the reservation, and commits. That keeps two customers from both seeing “1 seat left” and both buying it.
What goes wrong when this is misunderstood? A team uses a plain SELECT, then later runs UPDATE without locking first. Under load, two requests read the same stock value, both pass validation, and both decrement it. Users see duplicate confirmations, inventory goes negative, and support tickets spike. In logs you may see a mix of duplicate order IDs, “could not serialize access” or lock timeout messages, and a sudden rise in 500/409 responses during traffic peaks.
In another common failure, the team locks the row but keeps the transaction open while calling a payment gateway. That turns a 20 ms database action into a 2 second lock hold. The visible symptom is request pileup: dashboards show growing latency, DB sessions waiting on locks, and customers timing out even though the database itself is not “down.”
-- PostgreSQL demo: pessimistic locking with two sessions.
-- The SQL below is valid, but to see the blocking/failure path,
-- run the Session A block in one window and the Session B block in another.
DROP TABLE IF EXISTS inventory;
CREATE TABLE inventory (
sku text PRIMARY KEY,
quantity integer NOT NULL CHECK (quantity >= 0)
);
INSERT INTO inventory (sku, quantity)
VALUES
('book-123', 1),
('pen-456', 5);
-- =========================
-- Session A: take the lock
-- =========================
BEGIN;
-- Lock the target row before changing it.
-- This prevents another transaction from updating the same row first.
SELECT sku, quantity
FROM inventory
WHERE sku = 'book-123'
FOR UPDATE;
-- The check and the update happen while the row is locked,
-- which avoids a lost update on the last item.
UPDATE inventory
SET quantity = quantity - 1
WHERE sku = 'book-123'
AND quantity > 0;
-- Keep the transaction open for a moment to simulate real contention.
-- In a real app, avoid long work while holding locks.
COMMIT;
-- =========================
-- Session B: try to take the same lock
-- =========================
BEGIN;
-- NOWAIT fails immediately instead of waiting.
-- If Session A is still open, PostgreSQL raises an error here.
-- If you remove NOWAIT, the session may wait until Session A commits.
SELECT sku, quantity
FROM inventory
WHERE sku = 'book-123'
FOR UPDATE NOWAIT;
-- Edge case: a non-locking read still works, but it does not protect the row.
SELECT sku, quantity
FROM inventory
WHERE sku = 'book-123';
ROLLBACK;
-- Final state after Session A commits.
SELECT *
FROM inventory
ORDER BY sku;Follow-up & Tricky Questions:
SELECT ... FOR UPDATE do? It locks the selected rows so other transactions cannot update them until commit or rollback. It is the classic SQL tool for pessimistic locking.NOWAIT? It tells the database to fail immediately instead of waiting for the lock. This is useful when you prefer a fast error over a slow queue.SKIP LOCKED? It tells the database to ignore rows that are already locked by another transaction. It is popular in worker queues where many processes claim different jobs.SELECT lock the row? Usually no. A normal read often does not block writers, so you need an explicit locking clause when correctness depends on reserving the row.Common Mistakes:
Memory Hook: “Pessimistic locking is putting your hand on the last donut.” You reserve the item first, then finish the transaction.
Cheat Sheet:
SELECT ... FOR UPDATE is the classic row-locking pattern.NOWAIT fails fast; SKIP LOCKED skips busy rows.Practice Tasks:
FOR UPDATE NOWAIT behaves.SKIP LOCKED and see how a queue worker could claim jobs safely.