DEV Community

umzzil nng
umzzil nng

Posted on Originally published at oraerror.com

PostgreSQL 40P01 Error: Causes and Solutions Complete Guide

PostgreSQL Error 40P01: deadlock detected

A deadlock occurs when two or more transactions are waiting for each other to release locks, creating a circular dependency that can never be resolved on its own. PostgreSQL's built-in deadlock detector periodically checks for these cycles and resolves them by forcibly rolling back one of the involved transactions and returning error code 40P01. While a single deadlock won't bring down your database, frequent occurrences signal a serious design flaw that will hurt application reliability and throughput.


Top 3 Causes & SQL Examples

1. Inconsistent Lock Ordering

The most classic cause. Two transactions acquire locks on the same rows but in opposite orders, guaranteeing a deadlock under concurrent load.

-- Transaction A locks row 1, then tries to lock row 2
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- Transaction B (concurrent) locks row 2, then tries to lock row 1 → DEADLOCK
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;

-- Fix: always lock rows in the same sorted order
BEGIN;
SELECT id FROM accounts
WHERE id IN (1, 2)
ORDER BY id   -- enforce consistent lock order
FOR UPDATE;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
Enter fullscreen mode Exit fullscreen mode

2. Implicit Locks from Foreign Keys

PostgreSQL acquires implicit row-level locks during INSERT, UPDATE, and DELETE. When foreign key relationships exist, modifying a parent row places a share lock on related child rows. If another transaction is simultaneously modifying those child rows, a deadlock cycle can form silently.

-- Risky pattern: child and parent updated in different transaction order
-- Fix: explicitly lock the parent row first
BEGIN;

-- Step 1: Explicitly lock the parent first
SELECT id FROM orders WHERE id = 100 FOR UPDATE;

-- Step 2: Then safely modify child rows
UPDATE order_items
SET quantity = quantity - 1
WHERE order_id = 100 AND product_id = 55;

UPDATE orders
SET total_amount = total_amount - 2500
WHERE id = 100;

COMMIT;
Enter fullscreen mode Exit fullscreen mode

3. Unsorted Batch Updates

When multiple workers update the same set of rows without a consistent ordering, the probability of deadlocks increases dramatically with concurrency.

-- Dangerous: random row order in batch update
UPDATE inventory
SET stock = stock - 1
WHERE product_id IN (301, 205, 412, 108);

-- Safe: sort IDs before acquiring locks
BEGIN;
SELECT product_id FROM inventory
WHERE product_id IN (108, 205, 301, 412)
ORDER BY product_id   -- consistent ascending order
FOR UPDATE;

UPDATE inventory
SET stock = stock - 1
WHERE product_id IN (108, 205, 301, 412);
COMMIT;
Enter fullscreen mode Exit fullscreen mode

Quick Fix Solutions

Implement retry logic — Deadlocks are transient by nature. Catch 40P01 at the application layer and retry with exponential backoff:

-- Monitor active lock waits to diagnose deadlocks
SELECT
  blocked.pid AS blocked_pid,
  blocking.pid AS blocking_pid,
  blocked.query AS blocked_query,
  blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
Enter fullscreen mode Exit fullscreen mode

Set lock_timeout to avoid indefinite waits and fail fast instead:

-- Session-level: fail if lock not acquired within 3 seconds
SET lock_timeout = '3s';
Enter fullscreen mode Exit fullscreen mode

Prevention Tips

1. Keep transactions short. The longer a transaction holds locks, the higher the chance of conflicts. Never perform external API calls, user interactions, or heavy computation inside a transaction block. Use idle_in_transaction_session_timeout to auto-terminate stale transactions:

-- postgresql.conf
idle_in_transaction_session_timeout = '30s'
Enter fullscreen mode Exit fullscreen mode

2. Always enforce a consistent resource acquisition order. Establish a team-wide convention — for example, always lock rows by ascending primary key. Document this rule and enforce it in code reviews. Combined with application-level retry logic for 40P01 and 40001 (serialization_failure), this two-layer defense handles the vast majority of deadlock scenarios in production.


Related Errors

Code Name Relationship
40001 serialization_failure Similar retry requirement under SERIALIZABLE isolation
55P03 lock_not_available Raised by NOWAIT; a deadlock-avoidance alternative
57014 query_canceled Triggered by lock_timeout; used as a deadlock prevention safeguard

📖 Want a more detailed guide?
Check out the full in-depth version (Korean) on oraerror.com — includes detailed analysis, additional SQL examples, and prevention tips.

Top comments (0)