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_timeout at 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 LOCKED for 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.