PostgreSQL deadlock detected: use the lock graph to repair concurrency order
A deadlock is a cycle of mutually waiting transactions. PostgreSQL aborts one to protect correctness; the lasting fix is usually in application access order.
Maintained by Kevin · Ovalk
Scope and prerequisites
PostgreSQL transactions and row/table locks. Diagnostics require appropriate monitoring privileges; managed database access may be limited.
Capture the deadlock error and transaction boundaries. Queries and logs can contain customer data. Prefer EXPLAIN without ANALYZE for an initial plan; EXPLAIN ANALYZE actually executes the statement.
Common symptoms
- Applications receive deadlock detected.
- The same business action fails intermittently and succeeds on retry.
- High-concurrency writes increase tail latency.
1. Capture the complete context
Deadlock detail in PostgreSQL logs includes the wait relationship and SQL. Join that evidence to request traces, transaction boundaries, and table access order before attempting a fix.
SHOW log_lock_waits;
SHOW deadlock_timeout;
SELECT pid, wait_event_type, wait_event, state, query FROM pg_stat_activity WHERE state <> 'idle';2. Find inconsistent lock acquisition
A common pattern is one transaction locking orders then inventory while another does the reverse. Apply a consistent resource order, reduce transaction scope, and keep remote calls outside transactions.
SELECT locktype, relation::regclass, mode, granted, pid FROM pg_locks WHERE NOT granted OR relation IS NOT NULL;3. Use observable, bounded retry
An idempotent transaction can retry deadlock errors with bounded backoff, but retries must emit metrics and business context rather than silently hiding a systemic pattern.
SELECT datname, deadlocks, stats_reset FROM pg_stat_database WHERE datname = current_database();Interpret the evidence
| Observation | What to check next |
|---|---|
| SQLSTATE 40P01 | This identifies a deadlock. The victim transaction is aborted; retry the whole transaction with a bounded policy, not just its final statement. |
| Lock waits without a deadlock error | A blocker is not necessarily a cycle. Inspect pg_blocking_pids and transaction age before terminating a session. |
| No cycle visible after the error | PostgreSQL may already have broken the cycle. Use the recorded deadlock detail rather than expecting a later snapshot to reproduce it. |
Illustrative diagnosis
A hypothetical example to explain the reasoning, not a reported customer incident or a claim of testing on your stack.
Transaction A updates inventory row 10 then row 20, while transaction B updates 20 then 10. Each can hold the row the other needs. Sorting identifiers before acquiring locks addresses this particular cycle; blindly raising a timeout does not. Reproduce the order in a test database and include all statements in the transaction, not only the query that reported the error.
Verify recovery
- Run concurrent versions of the affected business action in staging and confirm both database invariants and the absence of the same lock cycle.
- Compare deadlock counts, retry counts and latency under representative load. A hidden retry loop can make errors appear fixed while latency worsens.
Rollback and stopping point
Deploy lock-order or retry changes with a reversible application release. A code rollback cannot undo duplicate external side effects; confirm idempotency and avoid mixing old/new lock orders during rollout.
Prevention and long-term repair
- Document lock ordering for cross-table updates.
- Keep transactions limited to necessary database work.
- Alert on lock waits, transaction duration, and deadlocks by service.
References and corrections
Use the documentation for your installed version. The references below explain the underlying behavior; commands still need environment-specific validation.
Report a correction to Kevin — include the page URL, version and a redacted reproduction. See our editorial policy.