Postgres Isolation Level Explained
In this article, I will use PostgreSQL with Docker and pgcli to explain isolation levels in PostgreSQL.
Prerequisites
Setup
Create a temporary database with Docker
docker run --rm \
--name postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=app \
-p 5432:5432 \
postgres:18
Connect with pgcli
pgcli postgresql://postgres:postgres@localhost:5432/app
Create shows table
CREATE TABLE shows (
id BIGSERIAL PRIMARY KEY,
show_name TEXT NOT NULL,
available_seat INT NOT NULL DEFAULT 0,
status VARCHAR(255) NOT NULL
);
Insert data
INSERT INTO shows (show_name, available_seat, status)
VALUES
('Taylor Swift Concert', 1, 'inactive'),
('Football Final', 1, 'inactive'),
('Movie Premiere', 1, 'active');
Phenomena
Dirty Read
Concept: A transaction is allowed to read uncommitted changes made by another transaction.

A dirty read cannot be shown in a Postgres example because dirty reads are not allowed in Postgres.
Non-Repeatable Read
Concept: One of the rows you’ve queried at different stages of a transaction may be different because the rows may be updated by other transactions.


Phantom Read
Concept: The records you’ve queried at different stages of a transaction may be different because rows may be added or removed by other transactions.


Lost Update
Concept: Occurs when two transactions read the same data and try to update it with different values, and one of the updates is lost.


Serialization Anomaly
Concept: The result of successfully committing a group of transactions is inconsistent with all possible orderings of running those transactions one at a time.
We will explain this more clearly in Errors for Serialization Anomalies.
Isolation Level
| Isolation Level | Dirty Read | Non-repeatable Read | Phantom Read | Lost Update | Serialization Anomaly |
|---|---|---|---|---|---|
| READ COMMITTED | ✅ Prevented | ❌ Can happen | ❌ Can happen | ⚠️ Can happen | ❌ Can happen |
| REPEATABLE READ | ✅ Prevented | ✅ Prevented | ✅ Prevented* | ✅ Prevented* | ❌ Can happen |
| SERIALIZABLE | ✅ Prevented | ✅ Prevented | ✅ Prevented | ✅ Prevented* | ✅ Prevented |
Errors in Repeatable Read and Serializable
Repeatable Read Error Example:
Repeatable read ERROR: could not serialize access due to concurrent update

When both transactions update the same row of the data, the second transaction will fail.
Serializable Error Example:
Isolation Level: Repeatable Read

Given scenario
inactive
inactive
active
active
In Transaction 1, we update those inactive rows to active. In Transaction 2, we update those active rows to inactive.
Since both transactions do not touch the same rows, both can be committed, and as a result, the statuses are flipped.
active
active
inactive
inactive
Isolation Level: Serializable
Serializable ERROR: could not serialize access due to read/write dependencies among transactions

Given scenario
inactive
inactive
active
active
In Transaction 1, we update those inactive rows to active. In Transaction 2, we update those active rows to inactive.
However, Postgres at the Serializable isolation level will detect a potential serial path.
The potential serial path:
- Serial Order Transaction 1 then Transaction 2:
- Transaction 1 would convert all inactive rows to active rows → the table becomes all active, then T2 would update all active rows to inactive → final state: all inactive.
- Serial Order Transaction 2 then Transaction 1:
- Transaction 2 would convert all active rows to inactive rows → the table becomes all inactive, then T1 would update all inactive rows to active → final state: all active.
However, in this example, the result will contain two active rows and two inactive rows, which does not match either possible final state—all inactive or all active. Therefore, the second transaction is not allowed and must be retried in a new transaction.