Overview
A deadlock is a circular lock wait: transaction A holds a lock B needs while B holds a lock A needs, so neither can proceed. PostgreSQL detects the cycle once a lock wait exceeds deadlock_timeout (default 1 s), aborts one transaction with error 40P01, and lets the others continue. The application must catch 40P01 and retry the whole transaction. Deadlocks surface under concurrent load, usually when two code paths update the same rows in different orders.
Rules
- Prevent them structurally: acquire locks in a consistent order everywhere, such as updating rows in ascending primary-key order. This is the PostgreSQL docs’ primary advice.
- Which transaction is aborted “is difficult to predict and should not be relied upon”. Leave
deadlock_timeoutat its default unless profiling says otherwise: the cycle check is expensive, so a lower value adds overhead to every lock wait and a higher one delays reporting real deadlocks. - Log every
40P01. Recurring ones are a lock-ordering bug, not noise to retry forever. - Keep transactions short, use
SELECT ... FOR UPDATE SKIP LOCKEDfor queue workers, and consider optimistic-locking for low-conflict read-modify-write. - Foreign key checks lock the referenced row and can join a cycle (foreign-key). Serializable and Repeatable Read raise serialization failures (
40001), not deadlocks, but need the same retry logic (transaction-isolation).
-- Transaction A -- Transaction B
UPDATE accounts SET bal = bal - 50 WHERE id = 1; UPDATE accounts SET bal = bal - 50 WHERE id = 2;
UPDATE accounts SET bal = bal + 50 WHERE id = 2; UPDATE accounts SET bal = bal + 50 WHERE id = 1;
-- Each waits on the other; Postgres aborts one. Fix: always update the lower id first.Related
- postgres: lock monitoring and
deadlock_timeout. - transaction-isolation: serialization failures need the same retries.
- optimistic-locking: avoids row locks entirely.
- foreign-key: FK checks take locks.
- connection-pool: lock waits hold pool connections.