Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Postgres By James Joyner IV · · 9 min read

PostgreSQL Error Guide: 'could not serialize access due to concurrent update' Serialization Failures

Fix PostgreSQL 'could not serialize access due to concurrent update': understand REPEATABLE READ/SERIALIZABLE conflicts and add transaction retry logic.

  • #postgres
  • #troubleshooting
  • #errors
  • #concurrency

Exact Error Message

ERROR:  could not serialize access due to concurrent update

When PostgreSQL runs under the SERIALIZABLE isolation level, the message can instead read:

ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during write.
HINT:  The transaction might succeed if retried.
SQLSTATE: 40001

What the Error Means

This error is raised when two transactions running under REPEATABLE READ or SERIALIZABLE isolation interfere with each other in a way that would break the snapshot guarantee. Under these isolation levels, each transaction sees a consistent snapshot of the database taken at the start of the transaction. If a row a transaction wants to update has already been modified and committed by another transaction since that snapshot was taken, PostgreSQL cannot safely apply the change without violating isolation, so it aborts the transaction with SQLSTATE 40001.

Unlike a deadlock, this is not a lock cycle — it is a snapshot conflict. PostgreSQL is telling you the transaction is logically impossible to serialize, and the correct response is almost always to roll back and retry the whole transaction. The HINT makes this explicit: “The transaction might succeed if retried.”

Common Causes

  • REPEATABLE READ write conflicts. Two transactions read the same row, then both try to update it. The first to commit wins; the second fails with this error because the row changed under its snapshot.
  • SERIALIZABLE read/write dependencies. Even read-only patterns can trigger it. SERIALIZABLE uses Serializable Snapshot Isolation (SSI), which detects dangerous dependency cycles between transactions and aborts a “pivot” transaction.
  • Optimistic concurrency without retry. Applications that adopt high isolation levels for correctness but never wrap transactions in a retry loop surface every conflict as a hard failure.
  • Hot rows under load. Counters, inventory levels, account balances, or sequence-like rows that many transactions update concurrently are the classic hot spots.
  • Long-running transactions. The longer a transaction holds its snapshot, the larger the window for a concurrent commit to invalidate it.

How to Reproduce the Error

Open two psql sessions and run them in an interleaved order. Session 1:

BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;  -- reads 500

Session 2 (while session 1 is still open):

BEGIN ISOLATION LEVEL REPEATABLE READ;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- succeeds

Back in Session 1:

UPDATE accounts SET balance = balance - 50 WHERE id = 1;
-- ERROR:  could not serialize access due to concurrent update

Session 1’s snapshot predates Session 2’s commit, so its update is rejected.

Diagnostic Commands

Check the default isolation level configured for the cluster:

psql -c "SHOW default_transaction_isolation;"

Inspect which sessions are currently running under stricter isolation and how long they have been open:

psql -c "SELECT pid, state, now() - xact_start AS xact_age, left(query, 60) AS query \
FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start;"

Count serialization-related rollbacks via database statistics (high xact_rollback relative to xact_commit is a clue):

psql -c "SELECT datname, xact_commit, xact_rollback FROM pg_stat_database \
WHERE datname = current_database();"

Find the error in the server log, including the conflicting statements:

sudo journalctl -u postgresql --no-pager | grep -B2 -A2 "could not serialize" | tail -40

Check current active transactions waiting on the same relations:

psql -c "SELECT pid, wait_event_type, wait_event, left(query,50) AS query \
FROM pg_stat_activity WHERE state = 'active';"

Step-by-Step Resolution

  1. Confirm the isolation level. This error only occurs under REPEATABLE READ or SERIALIZABLE. Verify with SHOW default_transaction_isolation; and check whether the application sets SET TRANSACTION ISOLATION LEVEL explicitly.

  2. Add retry logic in the application. This is the primary fix. Catch SQLSTATE 40001 and re-run the entire transaction (not just the failing statement) with a small exponential backoff. The snapshot is taken fresh on retry, so the second attempt usually succeeds.

    for attempt in 1..MAX_RETRIES:
        try:
            begin; ... commit;
            break
        except SQLSTATE 40001:
            rollback; sleep(backoff(attempt))
  3. Shorten transactions. Reduce the snapshot window by moving non-database work (network calls, computation) outside BEGIN ... COMMIT. Smaller transactions conflict less.

  4. Reconsider isolation level where appropriate. If a particular workload does not require serializable correctness, READ COMMITTED (the default) never raises this error because each statement re-reads the latest committed data. Only raise isolation where the business logic genuinely needs it.

  5. Reduce hot-row contention. For counters and aggregates, consider techniques like row partitioning, advisory locks, or SELECT ... FOR UPDATE under READ COMMITTED instead of optimistic snapshot isolation.

Prevention and Best Practices

  • Treat SQLSTATE 40001 as an expected, transient condition — never a fatal one. Every transaction at REPEATABLE READ or higher must be wrapped in retry logic.
  • Keep transactions short and avoid holding snapshots open across user input or slow external calls.
  • Use the lowest isolation level that satisfies correctness; default READ COMMITTED avoids serialization failures entirely for many workloads.
  • Monitor xact_rollback trends in pg_stat_database to catch rising serialization conflicts before they degrade throughput.
  • Document which code paths run under elevated isolation so reviewers know retry handling is mandatory there.
  • For triage of repeated conflicts, the free incident assistant can turn a serialization log block into a likely hot-row culprit and suggested fix.
  • deadlock detected (SQLSTATE 40P01) — a true lock cycle rather than a snapshot conflict, but also resolved by retry.
  • could not serialize access due to read/write dependencies among transactions — the SERIALIZABLE (SSI) variant of the same family.
  • canceling statement due to lock timeout — a different concurrency failure caused by waiting too long for a lock.
  • restart transaction hints in client drivers — the driver-level surfacing of 40001.

Frequently Asked Questions

Why does this happen under REPEATABLE READ but not READ COMMITTED? Under READ COMMITTED, each statement sees the latest committed data and re-evaluates rows that changed, so updates to concurrently modified rows simply proceed against the new value. REPEATABLE READ pins a single snapshot for the whole transaction, so a row that changed after the snapshot cannot be updated without violating that guarantee.

Should I just lower the isolation level to make the error go away? Only if the workload does not need the stronger guarantee. Elevated isolation is chosen to prevent anomalies like lost updates or write skew. If you genuinely need it, keep it and add retry logic instead of weakening correctness.

Is retrying safe, or could it double-apply an update? Retrying is safe because the failed transaction is fully rolled back — none of its writes were committed. The retry starts a brand-new transaction with a fresh snapshot. Ensure the application retries the entire transaction, not just the failing statement.

How many retries should I allow? Typically three to five attempts with exponential backoff and jitter. If a transaction repeatedly fails after that, it usually signals extreme contention on a hot row that needs a design change rather than more retries.

Does this error indicate data corruption? No. It is a normal concurrency-control outcome that protects correctness. No data is lost or corrupted; the aborted transaction simply made no changes.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.