DEV Community

umzzil nng
umzzil nng

Posted on Originally published at oraerror.com

PostgreSQL 40001 Error: Causes and Solutions Complete Guide

PostgreSQL Error 40001: Serialization Failure — What It Is and How to Fix It

PostgreSQL error code 40001 (serialization_failure) occurs when two or more concurrent transactions conflict in a way that would violate the guarantees of the SERIALIZABLE or REPEATABLE READ isolation level. PostgreSQL's concurrency control mechanism detects the conflict and forcibly rolls back one of the transactions to maintain data consistency. This is expected behavior, not a bug — but your application must be prepared to handle and retry it.


Top 3 Causes

1. SSI (Serializable Snapshot Isolation) Read-Write Conflict

When Transaction A reads a set of rows and Transaction B modifies or inserts rows that fall within A's read set, PostgreSQL detects a non-serializable dependency cycle and aborts one transaction.

-- Transaction A (reads aggregate)
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT SUM(amount) FROM orders WHERE customer_id = 42;

-- Transaction B (inserts into the same range — triggers conflict)
BEGIN ISOLATION LEVEL SERIALIZABLE;
INSERT INTO orders (customer_id, amount, status)
VALUES (42, 500.00, 'pending');
COMMIT;

-- Transaction A then tries to commit → gets ERROR 40001
COMMIT;
Enter fullscreen mode Exit fullscreen mode

2. Concurrent UPDATE on the Same Row (REPEATABLE READ)

Under REPEATABLE READ, if two transactions both read the same row and then attempt to update it, the second one to commit will fail with 40001 because its snapshot is now stale.

-- Session 1
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE account_id = 1;  -- reads 1000

-- Session 2 (commits first)
BEGIN ISOLATION LEVEL REPEATABLE READ;
UPDATE accounts SET balance = balance - 200 WHERE account_id = 1;
COMMIT;  -- succeeds

-- Session 1 (tries to commit after Session 2)
UPDATE accounts SET balance = balance - 300 WHERE account_id = 1;
COMMIT;  -- ERROR: 40001 serialization_failure
Enter fullscreen mode Exit fullscreen mode

3. Phantom Read Prevention via Predicate Locks

SERIALIZABLE uses predicate locks to prevent phantom reads. If Transaction A scans a range with a WHERE clause and Transaction B inserts a row matching that predicate, PostgreSQL raises 40001 to prevent the phantom read anomaly.

-- Transaction A: scans for pending orders
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM orders WHERE status = 'pending' AND region = 'US';

-- Transaction B: inserts a matching row
BEGIN ISOLATION LEVEL SERIALIZABLE;
INSERT INTO orders (status, region, amount) VALUES ('pending', 'US', 750);
COMMIT;

-- Transaction A: commits → ERROR 40001 (phantom prevention)
INSERT INTO summary (region, total) VALUES ('US', 750);
COMMIT;
Enter fullscreen mode Exit fullscreen mode

Quick Fix Solutions

1. Always implement a retry loop. This is the most important fix.

-- PL/pgSQL retry wrapper example
DO $$
DECLARE
    retries INT := 0;
BEGIN
    LOOP
        BEGIN
            SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

            UPDATE inventory
               SET stock = stock - 1
             WHERE product_id = 99 AND stock > 0;

            COMMIT;
            EXIT;  -- success, exit loop

        EXCEPTION WHEN serialization_failure THEN
            retries := retries + 1;
            IF retries > 5 THEN
                RAISE EXCEPTION 'Max retries exceeded on serialization_failure';
            END IF;
            PERFORM pg_sleep(0.1 * retries);  -- exponential backoff
            ROLLBACK;
        END;
    END LOOP;
END;
$$;
Enter fullscreen mode Exit fullscreen mode

2. Use SELECT ... FOR UPDATE with READ COMMITTED when full serializability is not required.

BEGIN;  -- default READ COMMITTED
SELECT stock FROM inventory
 WHERE product_id = 99
   FOR UPDATE;  -- pessimistic lock, avoids SSI conflicts entirely

UPDATE inventory SET stock = stock - 1 WHERE product_id = 99;
COMMIT;
Enter fullscreen mode Exit fullscreen mode

Prevention Tips

Keep transactions short. The longer a transaction holds open, the higher the chance of conflicting with another. Remove any external API calls, file I/O, or heavy computation from inside transaction blocks.

Choose the right isolation level. Don't default to SERIALIZABLE unless you truly need it. Use READ COMMITTED (PostgreSQL default) with explicit locking (FOR UPDATE, FOR SHARE) for most OLTP workloads. Reserve SERIALIZABLE for use cases that genuinely require anomaly-free consistency, such as financial ledgers or audit-critical workflows.

-- Check serialization failure stats per database
SELECT datname,
       conflicts,
       deadlocks,
       xact_rollback
  FROM pg_stat_database
 WHERE datname = current_database();
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Error 40001 is not a system failure — it is PostgreSQL working correctly. Build retry logic into every application that uses SERIALIZABLE or REPEATABLE READ, use exponential backoff, and keep your transactions as short as possible.


📖 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)