PostgreSQL error 40P01 (Deadlock Detected) explained: what it means, why it happens, and how to fix it — with copy-paste code examples.
Check the server log for the deadlock detail showing the two blocked statements. If that does not apply, ensure all code paths lock rows in a consistent global order — the full checklist is below.
SQLSTATE: 40P01
Official name: Deadlock Detected
Service: PostgreSQL
The transaction was aborted because it was selected as the deadlock victim to break a deadlock cycle.
-- Always lock parent rows before children, in id order:
SELECT id FROM accounts WHERE id = ANY($1) ORDER BY id FOR UPDATE;
Ordering locks by a stable key (e.g. id) across all transactions prevents the cycles that cause deadlock 40P01.
Wrap in transaction.atomic() and retry on OperationalError caused by deadlock.
from django.db import transaction, OperationalError
@transaction.atomic
for attempt in range(5):
try:
transfer()
break
except OperationalError:
if attempt == 4: raise
continue
Use a retry loop catching OperationalError.isinstance for code 40P01.
from sqlalchemy.exc import OperationalError
for attempt in range(5):
try:
session.execute(stmt)
session.commit()
break
except OperationalError as e:
if e.orig.sqlstate == '40P01':
session.rollback(); continue
raise
Use ActiveRecord's retriable? with a retry block around the transaction.
ActiveRecord::Base.transaction do
# work
end.recoverable? # then retry
Most often this happens when two transactions lock resources in opposite orders, forming a cycle, or when locking rows in application-defined order that differs across requests.
Check the server log for the deadlock detail showing the two blocked statements.
This page documents fixes for: django, sqlalchemy, rails.
Recommendations are editorial — DB Error Reference takes no payment or affiliate fees for tool listings.
This page is based on the official PostgreSQL documentation linked below and adds practical troubleshooting guidance on top.