PostgreSQL error 40001
could not serialize access due to concurrent update
Your transaction runs at REPEATABLE READ or SERIALIZABLE, and another transaction changed data in a way that breaks that promise, so PostgreSQL cancelled yours. This is expected: roll back and run the whole transaction again.
ERROR: could not serialize access due to concurrent update
Tested on PostgreSQL 18.6 · Updated 9 October 2026
What it means
PostgreSQL’s stricter isolation levels promise that your transaction works from one consistent view
of the data. REPEATABLE READ promises you won’t see other transactions’ changes part-way through;
SERIALIZABLE also promises the result is the same as if the transactions had run one after another.
When a concurrent transaction makes that promise impossible to keep, PostgreSQL cancels yours rather
than let it act on stale data.
It isn’t a bug or a broken database. The documentation is direct about it: applications using these
levels must be ready to retry. The SQLSTATE is always 40001 (serialization_failure); the wording
varies:
| Message | When |
|---|---|
could not serialize access due to concurrent update | You tried to update, delete or lock a row that another transaction changed and committed after your transaction began. |
could not serialize access due to concurrent delete | The same, for a row another transaction deleted. |
could not serialize access due to read/write dependencies among transactions | SERIALIZABLE only: the mix of reads and writes couldn’t have happened in any one-at-a-time order. |
The transaction is aborted. Until you roll back, every statement in it fails with current transaction is aborted.
Common causes
- Two transactions change the same row: a counter, a balance, a stock level, a “last seen” time.
Under
REPEATABLE READthe second one to write fails. - Read, decide, then write. At
SERIALIZABLE, two transactions read the same rows, each writes something based on what it read, and together they break a rule neither broke alone. - The isolation level isn’t the one you think. A framework, ORM or
default_transaction_isolationsetting makes every transactionREPEATABLE READorSERIALIZABLE. - Long transactions, which give others more time to change what they read.
How to fix it
Retry the whole transaction
Roll back, then run the transaction again from BEGIN, including the reads and the code that decides
what to write. Retrying only the failed statement doesn’t work, because the decision was based on old
data. A sketch with node-postgres:
async function inTransaction(pool, work, attempts = 5) {
for (let attempt = 1; ; attempt++) {
const client = await pool.connect();
try {
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
const result = await work(client);
await client.query('COMMIT');
return result;
} catch (err) {
await client.query('ROLLBACK');
const retryable = err.code === '40001' || err.code === '40P01';
if (!retryable || attempt === attempts) throw err;
await new Promise((r) => setTimeout(r, Math.random() * 50 * attempt));
} finally {
client.release();
}
}
}
The same loop handles deadlocks (40P01). A failure can arrive
at COMMIT too, so the commit belongs inside the retried block.
Check which isolation level you’re using
SHOW transaction_isolation;
SHOW default_transaction_isolation;
PostgreSQL’s default is READ COMMITTED. There, an UPDATE that finds a row changed by a committed
transaction re-reads the new version and carries on instead of failing. That’s often what you want
for counters and balances, written as one statement:
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
But READ COMMITTED doesn’t protect read-then-write rules. In the reproduction below, the same two
transactions both commit at READ COMMITTED and leave nobody on call. If your code relies on such a
rule, keep SERIALIZABLE and retry.
Make failures rarer
- Keep transactions short, and do slow work outside them.
- For long read-only reports at
SERIALIZABLE, useBEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE. It may wait briefly for a safe snapshot, then can’t be cancelled by a serialisation failure. - At
SERIALIZABLE, give your queries indexes. A query that scans a whole table watches the whole table for conflicting writes.
Reproduce it
Repeatable read, concurrent update. On PostgreSQL 18.6, session A:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM seo_pgconn.accounts WHERE id = 1; -- 100
SELECT pg_sleep(2);
UPDATE seo_pgconn.accounts SET balance = balance - 10 WHERE id = 1;
While it sleeps, session B runs and commits
UPDATE seo_pgconn.accounts SET balance = balance + 5 WHERE id = 1;. Session A’s update then fails:
ERROR: could not serialize access due to concurrent update
If session B deletes the row instead, the message ends due to concurrent delete.
Serializable, write skew. A table says who’s on call, and the rule is that at least one doctor must be. Both sessions run, a second apart:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM seo_pgconn.on_call WHERE on_call; -- 2, so it’s safe to leave
UPDATE seo_pgconn.on_call SET on_call = false WHERE doctor = 'alice'; -- 'bob' in session B
SELECT pg_sleep(2);
COMMIT;
Session A commits. Session B’s COMMIT fails (here with \set VERBOSITY verbose, trimmed):
ERROR: 40001: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.
Bob stays on call. Run the same two transactions at READ COMMITTED and both commit:
doctor | on_call
--------+---------
alice | f
bob | f
(2 rows)
In Inlet
The query editor shows the server’s error where it reports it, and supports transactions with manual commit, so you can roll back and run the transaction again. With your own Anthropic API key, Ask Claude can explain the failed statement and suggest a fix.