ERROR: deadlock detected

Quick answer

Two transactions each hold a lock the other needs, so PostgreSQL breaks the cycle by aborting one (the victim). The durable fix is to acquire locks in a consistent order (e.g. always by ascending id), keep transactions short, and retry the aborted transaction — a retry usually succeeds once the other one finishes.

The exact error string

ERROR:  deadlock detected
DETAIL:  Process 1234 waits for ShareLock on transaction 5678; blocked by process 4321.
         Process 4321 waits for ShareLock on transaction 8765; blocked by process 1234.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,7) in relation "accounts"
SQLSTATE: 40P01

What it actually looks like

Terminal showing PostgreSQL ERROR: deadlock detected with DETAIL naming both waiting processes, HINT to see the server log, CONTEXT while updating tuple (0,1) in relation accounts, and ROLLBACK — followed by the server log entry containing the two conflicting UPDATE statements.
Captured on PostgreSQL 16.15 by running two transactions that update the same two rows in opposite order. Two details repay attention. ERROR: is followed by two spaces — useful if you are grepping logs. And the HINT is not boilerplate: the client error never shows the conflicting statements, while the server log does, naming both processes and exactly what each was running. That is the difference between guessing at a lock order and knowing it.

Postgres detected a cycle: process 1234 waits on 4321, and 4321 waits on 1234. Neither can ever proceed, so Postgres aborts one to free the other. The aborted transaction gets the error; the survivor commits normally.

Why deadlocks happen

A deadlock needs two transactions that take the same locks in opposite order. Transaction A locks row 1 then wants row 2; transaction B locks row 2 then wants row 1. Each waits for the other forever — until Postgres steps in:

-- Transaction A                  -- Transaction B
BEGIN;                            BEGIN;
UPDATE accounts SET ... WHERE id=1;
                                  UPDATE accounts SET ... WHERE id=2;
UPDATE accounts SET ... WHERE id=2;   -- waits for B
                                  UPDATE accounts SET ... WHERE id=1;  -- waits for A
-- ❌ deadlock — Postgres aborts one

Fix 1: lock in a consistent order (the real fix)

If every transaction touches rows in the same order, the opposite-order cycle can't form. Order your updates (and SELECT ... FOR UPDATE) deterministically:

-- Both transactions update in ascending id order — no cycle possible
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- locking reads: take the locks up front, in order
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;

Fix 2: retry the victim

Deadlocks are normal under concurrency — the right response is to retry. Catch SQLSTATE 40P01, roll back, and re-run the transaction (make it idempotent so a retry is safe):

import psycopg2, time

for attempt in range(3):
    try:
        with conn:                      # commits, or rolls back on error
            with conn.cursor() as cur:
                cur.execute("UPDATE accounts SET ... WHERE id = %s", (1,))
                cur.execute("UPDATE accounts SET ... WHERE id = %s", (2,))
        break                           # success
    except psycopg2.errors.DeadlockDetected:
        time.sleep(0.1 * (attempt + 1)) # back off, then retry

Fix 3: keep transactions short

The longer locks are held, the wider the window for a collision. Don't do slow work — HTTP calls, file I/O, waiting on user input — inside a transaction. Read what you need, write quickly, and commit. Adding the right indexes also shrinks lock scope, because Postgres locks fewer rows when it can target them precisely.

Get the conflicting statements out of the server log

The HINT: See server log for query details. line is not boilerplate, and it is the single most useful thing on the page. The client error deliberately withholds the SQL — another session's statements are not yours to see — but the server log records both sides:

2026-09-12 05:10:11.270 UTC [117] ERROR:  deadlock detected
2026-09-12 05:10:11.270 UTC [117] DETAIL:  Process 117 waits for ShareLock on transaction 734; blocked by process 116.
        Process 116 waits for ShareLock on transaction 733; blocked by process 117.
        Process 117: UPDATE accounts SET balance = balance + 10 WHERE id = 1;
        Process 116: UPDATE accounts SET balance = balance + 10 WHERE id = 2;
2026-09-12 05:10:11.270 UTC [117] CONTEXT:  while updating tuple (0,1) in relation "accounts"
2026-09-12 05:10:11.270 UTC [117] STATEMENT:  UPDATE accounts SET balance = balance + 10 WHERE id = 1;

Captured on PostgreSQL 16.15. The two Process N: lines are the whole diagnosis: they show each transaction's final statement, and reading them side by side makes the inverted lock order obvious — one transaction reached id = 1 last, the other reached id = 2 last, so each already held what the other was waiting for. On a managed service (RDS, Cloud SQL, Supabase) this is in the Postgres error log, not in your application logs.

Which transaction gets aborted — and why you can't predict it

It is natural to assume the choice is principled: the newer transaction loses, or the one holding fewer locks, or the one that has done less work. It is none of those. PostgreSQL aborts whichever backend's deadlock_timeout happens to expire first after the cycle has closed — the process that runs the next deadlock check is the one that finds the cycle, reports it, and dies. That is a timer race, and it is decided by microsecond-level arrival order you do not control.

Three runs of the same two-transaction cycle on PostgreSQL 16.15, varying only how long each transaction waited before reaching for the second row:

A blocks atB blocks atCycle closesAborted
t = 1st = 6st = 6sB
t = 6st = 1st = 6sA
t = 2.0st = 2.4st = 2.4sA

The first two rows look like a tidy rule — "whoever completes the cycle loses" — and the third breaks it. In row 3 A blocked first and still became the victim: the cycle closed at 2.4s, A had been waiting since 2.0s so its timer fired at 3.0s, and B's would not have fired until 3.4s. A got there first and A died. In rows 1 and 2 the two waits were five seconds apart, so the earlier waiter's timer had already fired and found nothing before the cycle existed, leaving the later one to discover it.

So neither "first in wins" nor "last in loses" is reliable. The only rule that holds is the timer one, and the gap between the two blocks relative to deadlock_timeout (1s by default) decides which case you land in.

What follows from this is the part that affects your code. You cannot designate one of the two code paths as "the one that will be retried." Either side can be the victim on any given occurrence, including the path you think of as primary or user-facing. So retry handling belongs on both transactions in the pair, not just the one that happened to lose while you were debugging — and the retry has to be safe on both sides. If only one path can tolerate replay, consistent lock ordering is not merely the better fix, it is the only one that works, because it removes the race instead of coping with its outcome.

All three rows reproduced on PostgreSQL 16.15 with the default deadlock_timeout = 1s, two UPDATEs per transaction in opposite order. Victim selection is an implementation detail of the detector, not a documented guarantee — which is precisely the reason not to build logic that assumes a particular side loses.

The settings that matter

SettingDefault (PG 16)Why it matters here
deadlock_timeout1sHow long a transaction waits before Postgres runs deadlock detection at all. Detection is not instant — it is deliberately deferred, because the check is expensive and most waits resolve on their own.
log_lock_waitsoffTurn it on. It logs every wait that exceeds deadlock_timeout, which surfaces the near-misses that precede a real deadlock.
lock_timeout0 (disabled)Caps how long a statement will wait for a lock. Setting it converts some would-be deadlocks into a quick, predictable failure you can retry.

One consequence of that first row is worth stating plainly: because detection only runs after a full second of waiting, a deadlock costs at least deadlock_timeout of latency before anything is reported. If deadlocks are frequent enough to matter, that delay is part of your p99 — another reason to fix the lock ordering rather than rely on retries alone.

The SQLSTATE is 40P01 (deadlock_detected). It belongs to class 40, transaction rollback — the same family as serialization failures — which is exactly why it is safe to retry: Postgres guarantees the aborted transaction changed nothing. Match on the code rather than the message text, since the message is localised.

Debugging checklist

Frequently Asked Questions

What does 'ERROR: deadlock detected' mean in Postgres?

Two (or more) transactions each hold a lock that the other needs, so neither can proceed. PostgreSQL detects the cycle and breaks it by aborting one transaction (the 'victim') with this error, letting the other continue. The DETAIL line names the processes and the locks involved.

What is the main way to prevent deadlocks?

Always acquire locks in a consistent order. If every transaction updates rows or tables in the same order (for example, always by ascending id), two transactions can never hold locks in the opposite order, so the cycle that causes a deadlock cannot form.

Should I retry after a deadlock?

Yes. A deadlock aborts only the victim transaction; retrying it usually succeeds because the other transaction has finished. Catch the deadlock (SQLSTATE 40P01), roll back, and retry the whole transaction a few times with a small backoff. Make the transaction idempotent so a retry is safe.

How do I read the DETAIL line?

The DETAIL shows 'Process A waits for ShareLock on transaction X; Process B waits for ShareLock on transaction Y' and the queries involved. It tells you which two statements locked rows in opposite order — the pair to reorder. Enable log_lock_waits to capture more context in the logs.

Do shorter transactions help?

Yes. The longer a transaction holds locks, the larger the window in which another transaction can collide with it. Keep transactions short, do not perform slow work (HTTP calls, user prompts) while holding locks, and commit as soon as the writes are done.

Can SELECT ... FOR UPDATE cause deadlocks?

Yes — it takes row locks, so two transactions locking the same rows in different orders can deadlock just like UPDATE. Lock rows in a deterministic order (for example SELECT ... FOR UPDATE ORDER BY id) and lock everything you need up front rather than incrementally.

References

More backend & build errors

Browse the full reference for Node.js, Python, Docker, and database errors — exact message, cause, and fix.

All Error References Postgres: too many clients Postgres: duplicate key
About the author

Pasindu Ishan is a Senior Software Engineer working on data streaming systems in Java, Kafka, Flink and PostgreSQL. These are the pages closest to that day-to-day work, and the ones where the fix is usually a matter of ordering or configuration rather than code. He builds the free in-browser tools at JSON Dev Tools.