Transaction isolation levels are the knob PostgreSQL gives you to control how much concurrent activity affects the data a transaction sees. Every database transaction runs at some isolation level — the default is READ COMMITTED — and that choice determines which consistency anomalies the application needs to handle.
The isolation level a transaction uses is not just a performance dial. It is a correctness decision. Code that reads data, makes a decision based on what it read, and then writes a result can produce wrong answers if the underlying data changes between the read and the write — a class of bugs that only appears under concurrency and only at certain isolation levels.
What Transaction Isolation Levels Mean
The SQL standard defines four isolation levels in terms of which anomalies they permit:
READ UNCOMMITTED — allows dirty reads (seeing uncommitted changes from other transactions). PostgreSQL does not implement this level: it maps READ UNCOMMITTED to READ COMMITTED, so dirty reads cannot occur.
READ COMMITTED — the PostgreSQL default. A new snapshot is taken at the start of each SQL statement within the transaction. Two SELECT statements in the same transaction can return different results if a concurrent transaction commits between them.
REPEATABLE READ — the snapshot is taken once at the start of the first query in the transaction and held for the entire transaction. Reads are stable. PostgreSQL’s MVCC implementation also prevents phantom reads at this level. Concurrent transactions that write to the same rows will cause one to be aborted with a serialization error.
SERIALIZABLE — builds on REPEATABLE READ by adding predicate locking. PostgreSQL tracks the data ranges each transaction read and detects whether any concurrent transaction’s writes could logically have changed the read result. If a conflict is detected, one transaction is aborted with ERROR: could not serialize access due to read/write dependencies. The application must retry.
Where Isolation Levels Matter in Practice
Agentic workloads — AI agents that query data, reason over it, and write back a decision or state update are read-modify-write patterns. Under READ COMMITTED, the data an agent read in step one may have changed before the agent writes in step three, resulting in a decision based on stale context. REPEATABLE READ prevents this by locking the agent’s view to a stable snapshot for the full transaction.
Analytics and reporting — long-running queries that join multiple tables need a consistent snapshot to produce meaningful aggregates. READ COMMITTED can return a mix of data from before and after a concurrent batch write. Running reports under REPEATABLE READ ensures the result reflects a single consistent state.
Schema migrations — a migration that reads existing rows to backfill a new column should use REPEATABLE READ to ensure the read and write phases operate on the same consistent dataset. Under READ COMMITTED, rows committed by other sessions after the migration begins may be missed or double-processed.
Test isolation-sensitive queries in a Vela branch before changing production settings. Try Database Branching
Why Isolation Levels Matter for Production Postgres
The default READ COMMITTED level is correct for most simple OLTP operations: short-lived transactions that write a single row rarely encounter anomalies. Problems appear when:
- Transactions are longer and span multiple reads and writes
- Application logic makes decisions based on read data before writing
- Concurrent batch processes update large sets of rows
- AI agents or automation scripts treat a Postgres query result as authoritative input to a subsequent action
Choosing a higher isolation level introduces retry complexity. SERIALIZABLE transactions can be aborted by PostgreSQL when a conflict is detected. The application must detect SQLSTATE 40001 (serialization failure) and retry the transaction from the beginning. Systems that use SERIALIZABLE need retry logic baked into their database access layer.
| Isolation Level | Dirty read | Non-repeatable read | Phantom read | Serialization anomaly |
|---|---|---|---|---|
| READ COMMITTED | Prevented | Possible | Possible | Possible |
| REPEATABLE READ | Prevented | Prevented | Prevented (in PG) | Possible |
| SERIALIZABLE | Prevented | Prevented | Prevented | Prevented |
How Isolation Levels Relate to Vela
Isolation level selection is easy to get wrong in staging because staged environments rarely replicate the concurrency patterns of production. A READ COMMITTED application that works fine in a single-user test environment can produce wrong results under production-level concurrent writes.
Vela branches give teams a production-like Postgres environment where isolation-sensitive queries can be validated against realistic data volumes and concurrent load patterns before changes ship. This is particularly useful when:
- Changing the default isolation level for a service or migration job
- Introducing agentic or AI workloads that read and write in the same transaction
- Verifying that SERIALIZABLE retry logic works correctly before deploying it
The branch starts from a consistent snapshot of the source database, so the isolation semantics the branch Postgres instance provides are the same as production. Changes made in the branch — including SET default_transaction_isolation experiments — are isolated from the source.
Operational Checks
- Check the current default with
SHOW default_transaction_isolation;and confirm it matches application expectations - Add retry logic for
SQLSTATE 40001before enabling SERIALIZABLE on any critical path - Review long-running transactions with
SELECT * FROM pg_stat_activity WHERE state = 'active' AND now() - query_start > interval '1 minute' - Confirm agentic and read-modify-write workflows use REPEATABLE READ or SERIALIZABLE explicitly
Related Vela Reading
Start with How Vela Works, Database Branching, Branch per PR, and the Vela articles library. For adjacent glossary terms, review Snapshot Isolation, MVCC (Multi-Version Concurrency Control), Transaction, and Deadlock Detection and Prevention.