PostgreSQLRunbook 0/4 done
40P01

PostgreSQL Error 40P01: Deadlock detected

Error↻ Retryable · exponential backoffHTTP 409 ConflictDatabase
TL;DR · 10-second read

Two transactions each hold a row lock the other needs, so Postgres aborted one after deadlock_timeout (1s). Lock rows in a consistent order and retry the aborted transaction.

Identify: does your log match?

What you see · exact log / response
ERROR:  deadlock detected
DETAIL:  Process 48211 waits for ShareLock on transaction 9123412; blocked by process 48207.
Process 48207 waits for ShareLock on transaction 9123410; blocked by process 48211.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,7) in relation "accounts"
  1. High

    Rows locked in inconsistent order

    Transaction A updates account 1 then 2 while B updates 2 then 1. Each waits on the other forever until the detector kills one.

  2. Medium

    Foreign keys take implicit share locks

    Inserting a child row takes FOR KEY SHARE on the parent. Concurrent updates of the parent and inserts of children can cycle.

  3. Low

    Long transactions widen the window

    Doing HTTP calls or heavy work inside a transaction keeps locks held for seconds, making collisions likely under load.

Fix

Lock rows in a deterministic order

✕ Broken · SQL
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = $from;
UPDATE accounts SET balance = balance + 100 WHERE id = $to;
COMMIT;
✓ Fixed · SQL
BEGIN;
SELECT id FROM accounts
WHERE id IN ($from, $to)
ORDER BY id
FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = $from;
UPDATE accounts SET balance = balance + 100 WHERE id = $to;
COMMIT;

Every transfer now acquires locks in ascending id order, so no cycle can form.

Verify: reproduce it on purpose

Reproduce locally · bash
$ psql -c "CREATE TABLE IF NOT EXISTS t(id int PRIMARY KEY, v int); INSERT INTO t VALUES (1,0),(2,0) ON CONFLICT DO NOTHING;" && (psql -c "BEGIN; UPDATE t SET v=1 WHERE id=1; SELECT pg_sleep(1); UPDATE t SET v=1 WHERE id=2; COMMIT;" & psql -c "BEGIN; UPDATE t SET v=2 WHERE id=2; SELECT pg_sleep(1); UPDATE t SET v=2 WHERE id=1; COMMIT;"; wait)

Run it against a dev environment to trigger 40P01 on purpose and test your error handler.

Prevent

  • Keep transactions short; no network calls inside them.
  • Set log_lock_waits = on to see waits before they deadlock.
  • Index foreign-key columns to shrink lock scope.

FAQ

Which transaction does Postgres kill in a deadlock?

The one whose deadlock_timeout expired and ran the detector. It is not predictable, so every participant must be retry-safe.

How do I see the queries involved?

The full statements are in the server log next to the ERROR line. On managed Postgres, check the provider's log explorer.

Is 40P01 the same as a serialization failure?

No. 40001 serialization_failure comes from REPEATABLE READ/SERIALIZABLE conflicts; both are in class 40 and both should be retried.

Related errors

Last verified 2026-10-16 · Source: Official Appendix A error codes (errcodes.txt in the postgres repo) · Official docs ↗ · ~6,600 searches/mo