PostgreSQL error: could not serialize access
ERROR: could not serialize access due to concurrent update
-- oppure, a livello Serializable:
ERROR: 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.
This error only exists at the Repeatable Read and Serializable isolation levels, and it is those levels working as designed: PostgreSQL could not produce a result consistent with the guarantees you asked for, so it aborted your transaction instead of silently returning something wrong.
What this error means
The two variants correspond to the two levels:
- "due to concurrent update" (Repeatable Read): your transaction tried to modify a row that another transaction changed and committed after your snapshot was taken. PostgreSQL can't apply your write to data you never saw, so it aborts you.
- "due to read/write dependencies among transactions" (Serializable): the SSI machinery found a pattern of reads and writes among concurrent transactions that could not correspond to any one-at-a-time execution order. Note the phrase in the docs and in practice: Serializable can abort transactions preventively — occasional false positives are part of the deal.
Either way the transaction was rolled back and, as the HINT says, would likely succeed if simply run again. SQLSTATE is 40001 in both cases.
Common causes
- Running at Repeatable Read or Serializable with concurrent writers on the same rows — hot rows and counters are the usual collision points.
- Long transactions: the older your snapshot, the more likely someone committed a conflicting change in the meantime.
- At Serializable, queries that read broadly: a sequential scan takes a predicate lock on the whole table, so it conflicts with any concurrent write to that table, relevant or not.
- An ORM or framework silently setting the isolation level — teams sometimes discover they've been running Repeatable Read only when this error appears under load.
How to diagnose it
- Confirm what level you're actually running:
SHOW default_transaction_isolation;and check whether the application sets it per transaction. - Log and count occurrences of SQLSTATE
40001: a low background rate under Serializable is normal and healthy; a spike tied to one endpoint identifies the colliding workload. - Identify the two sides of the conflict from application logs (which transactions touch the same rows or tables around the failure time). The error's
DETAILreason codes are mostly useful to confirm it's SSI at work, not to point at the peer.
How to fix it
- Retry the whole transaction on SQLSTATE
40001— including its reads, since the values read the first time are exactly what may have changed. Bounded attempts, small backoff, no side effects (emails, HTTP calls) inside the retried block. - Keep transactions short and touch as few rows as possible.
- At Serializable, make reads use indexes: precise index scans take narrow predicate locks and collide far less than sequential scans.
- If a hot spot generates constant retries, consider handling that one path with Read Committed plus explicit locking (
SELECT ... FOR UPDATE) instead — contention resolves by waiting instead of by aborting.
🔍
Related reading: Transaction isolation levels in practice — what each level guarantees, write skew, and a full section on retrying correctly.
