Home

PostgreSQL Isolation Levels Lab

A hands-on sandbox for watching PostgreSQL transaction isolation levels behave (and misbehave) across two psql sessions side by side. You run real interleaved transactions and see exactly when anomalies appear — and which isolation level or locking technique stops them.

This site is the reference handbook. The hands-on material is six small SQL demos in the repo, each runnable cold.

Three ways to use the lab

  • Manual — bring up the environment and copy-paste from the sql/ files. See Environment Setup.
  • Guided (in Cursor) — open the repo in Cursor and type /start-setup, then /start-1/start-6. An AI instructor walks you through setup and each demo, waiting for you at each step.
  • This website — browse and search the demos as reference pages whenever you want to look something up.

What you’ll explore

Three isolation levels and the anomalies they address:

LevelSnapshot?Stops non-repeatable reads?Stops write skew?
Read Committed (default)Per-statementNoNo
Repeatable ReadPer-transactionYesNo
Serializable (SSI)Per-transaction + predicate trackingYesYes (aborts with 40001)

Plus the pragmatic escape hatch: SELECT ... FOR UPDATE pessimistic row locking, which serializes access to hot rows without bumping isolation all the way to Serializable.

The demos

#DemoIsolation levelWhat it shows
1Read CommittedRead CommittedNon-repeatable reads — the same SELECT returns different values within one transaction.
2Repeatable ReadRepeatable ReadA stable snapshot, but writing a concurrently-modified row raises SQLSTATE 40001.
3Write SkewRepeatable ReadTwo transactions read overlapping data, write disjoint rows, and break a multi-row invariant. RR can’t catch it.
4SerializableSerializableSSI detects the read/write dependency cycle from demo 3 and aborts one transaction with 40001.
5SELECT … FOR UPDATERead CommittedPessimistic row locking serializes access without using Serializable.
6Balance TransferRead CommittedA stale-validation race on a money transfer, and the FOR UPDATE fix.

A recurring character: SQLSTATE 40001

Two demos end with ERROR: could not serialize access... / SQLSTATE 40001. That is not a setup mistake — it’s Postgres refusing to commit a transaction that would violate a serializable history. The correct application response is always to retry the whole transaction. The demos make this concrete.

Start with the Isolation Levels Primer, or jump straight to Environment Setup.