dbit.one© 2026
000
booting_
Loading experience0%
dbit.one

Isolation levels: what your database actually admits

Short version: an isolation level is not a performance setting, it is the list of outcomes your application is allowed to see. Between “read committed” and “serializable” sit anomalies with names and with a price: a balance driven below zero, a shift with no doctor on it, a document numbered twice. Reading in the docs that the database “supports snapshot isolation” is not enough — that is a property of executions, not of text.

11 min read

In short

  • An isolation level defines the set of admissible outcomes, not the speed. It has to be checked on executions rather than in documentation.
  • Snapshot isolation admits write skew: two transactions each see a consistent picture and together break the invariant.
  • Serializable in PostgreSQL is Cahill’s algorithm: it looks for a transaction with both an incoming and an outgoing anti-dependency and refuses to commit it.
  • Checking is done on a dependency graph; to recover the version order from observations, values are made unique — the Elle trick.

Two different promises in one sentence

“We provide snapshot isolation” is really two claims. First: nothing weaker ever happens — the anomalies this level is required to prevent will not occur. Second: it genuinely is SI, rather than something stricter wearing the same name.

Neither is checkable by reading code or documentation. They are properties of executions, not of text: they show up in a particular interleaving of transactions under a particular load. So executions have to be caught and interrogated.

Anomalies that have names

In 1999 Atul Adya’s thesis replaced the ANSI standard’s prose definitions with phenomena defined through a graph of dependencies between transactions. Using his names has a practical benefit: they leave no room for arguing about what exactly you observed.

Phenomenon
What it is
G0
a cycle of write-dependencies alone
G1a
a read of a value written by a transaction that then aborted
G1b
a read of an intermediate value its own writer later replaced
G1c
a cycle of write- and read-dependencies
G-single
a cycle containing exactly one anti-dependency — read skew
G2-item
a cycle containing more than one — write skew

The levels form a ladder: each forbids everything the one below it forbids, plus one more thing.

Level
What it adds to the prohibitions
read uncommitted
G0
read committed
G1a, G1b, G1c
snapshot isolation
G-single
serializable
G2-item

The entire practical difference between snapshot isolation and serializability is one anti-dependency. Read skew has one, and SI prevents it. Write skew has two, and SI cannot.

Write skew: taking out more than there is

The classic case looks innocent. The rule: the combined balance of a client’s two accounts must never go negative. Two transactions withdraw money — one from each account — and each checks the sum before withdrawing.

sql
-- transaction A                    -- transaction B
BEGIN;                              BEGIN;
SELECT sum(balance) FROM accounts   SELECT sum(balance) FROM accounts
 WHERE client = 7;  -- 200           WHERE client = 7;  -- 200
-- 200 - 150 >= 0, allowed          -- 200 - 150 >= 0, allowed
UPDATE accounts SET balance =       UPDATE accounts SET balance =
  balance - 150 WHERE id = 1;         balance - 150 WHERE id = 2;
COMMIT;                             COMMIT;
Both start at the same time, 100 on each account, the limit is on the sum

There is no write conflict: the transactions touch different rows. Each read a consistent snapshot and made a correct decision. Both commit — and the combined balance is minus one hundred. The invariant is broken although neither transaction broke it on its own.

The same shape appears outside money: the last on-call doctor takes themselves off the shift at the same moment as a colleague; two managers reserve the last slot in a warehouse; two operations assign the same number to a document. In every case one thing is read and another is written — and SI lets that pair through by construction.

What serializable does, and what it costs

Snapshot isolation’s remaining hole has a shape: any cycle it admits contains two consecutive anti-dependency edges. So it is enough to watch for a transaction with both an incoming and an outgoing one — the pivot — and refuse to let it commit.

That is SSI by Cahill, Röhm and Fekete (SIGMOD 2008), the algorithm PostgreSQL implements as SERIALIZABLE. The check is deliberately conservative: it also aborts some transactions that would in fact have been serializable. The price is throughput, never correctness.

Check it, do not believe it

Finding an anomaly needs a dependency graph: write-after-write, read-after-write and write-after-read edges between transactions. Then you look for cycles, and the shape of a cycle gives the phenomenon its name.

One difficulty makes the naive approach useless. To draw an edge between two writes you must know which came first — and from a history over ordinary values that is unrecoverable: two writes of 5 are indistinguishable. Asking the database for the order means taking the defendant’s word for it.

The way out comes from Elle (Kingsbury & Alvaro, VLDB 2020): values are append-only lists, and every write appends a globally unique element. Now a single read of [a, b, c] states, by itself, that a preceded b preceded c. The version order is recovered from observations rather than taken from the store’s internals — so a database that lied about its own ordering would be caught rather than believed.

The rest is machinery: Tarjan’s strongly connected components, and inside each a breadth-first search that finds the shortest cycle. That is not pedantry: “there is a cycle among these forty-one transactions” is not a bug report, and two transactions with their edges spelled out is.

The ladder: checked in both directions

Here is the part everything else exists for. Every level is held to two statements at once.

  • Sound. Over hundreds of seeded schedules the level never produced an anomaly it is required to prevent.
  • No stricter than advertised. There exists a schedule on which it does produce exactly what the next rung prevents.
text
read-uncommitted     sound over 150 schedules   admits G1b at seed 0
read-committed       sound over 150 schedules   admits G-single at seed 1
snapshot-isolation   sound over 150 schedules   admits G2-item at seed 25
serializable         sound over 150 schedules   admits nothing, commits 51%
The whole ladder: every rung sits where the literature puts it

The second half matters more than the first. Soundness alone proves almost nothing: an engine that aborted every transaction would be “clean” at every level, and a checker that found nothing would agree with it. Requiring each rung to exhibit the next rung’s forbidden phenomenon closes that hole — and it checks the checker, because one that could not find write skew under snapshot isolation would have nothing behind its acquittal of serializable either.

The last line follows the same logic. Under a contended workload serializable commits 51% of transactions, and that is asserted by a test rather than printed for information: an acquittal bought by aborting everything would be worthless.

What to do about it in an application

Raising everything to SERIALIZABLE is not the only answer and often not the best one. There are three practical routes, and they combine.

How to close an invariant your default level does not hold
  1. 01

    Give the invariant to the database

    Unique indexes, CHECK constraints and foreign keys are enforced regardless of isolation level. “One active document per client” is cheaper as a partial unique index than as a guard in code.

  2. 02

    Lock what you read in order to decide

    SELECT … FOR UPDATE turns the rows you read into a write conflict, which makes write skew on those rows impossible. It works when the set of rows is known in advance; for a WHERE condition matching rows that do not exist yet, it does not.

  3. 03

    Use serializable where the invariant cannot be expressed otherwise

    And add a retry on serialization failure in the same change: without it the level turns into an error generator for the user.

Worth keeping separate: retrying a transaction and retrying a request are different things. A client retry without an idempotency key creates a second operation, and no isolation level saves you from that — see the piece on payment idempotency.

What this method cannot do

  • No predicate anti-dependencies. Plain G2 needs predicate reads of the SELECT … WHERE kind; this workload has none, there is nothing to infer them from, and claiming to check G2 would claim more than the evidence supports. Only G2-item is checked.
  • Passing is not proof. Hundreds of schedules from a space that is incomparably larger. Good fuzzing, not a theorem; TLA+ specifications of SSI prove what sampling cannot.
  • The checker is not complete on everything. It finds a shortest cycle through each component member rather than enumerating every cycle: enough for these histories, but promising exhaustive search would be false.
  • This is not a database. No durability, no recovery, no indexes, no garbage collection of old versions: an isolation engine and nothing more.

What to ask yourself and your contractor

Five isolation questions that have a checkable answer
  • What isolation level do you run by default, and who on the team can name it without opening the config?
  • Which application invariants survive two concurrent transactions that read the same thing and write different things?
  • Is there a retry on serialization failure in the code — and is it covered by a test?
  • Which rules are enforced by the database (unique indexes, CHECK) and which are guarded in application code?
  • What supports the claim that the chosen level prevents what it was chosen for: a measurement, or the vendor’s documentation?

These are the questions we ask ourselves on projects where wrong data costs more than downtime: financial systems, accounting and stock balances, CRM and ERP. The rest of the technical write-ups live in the Engineering section.

Frequently asked questions

We run PostgreSQL with default settings. How bad is that?

The default there is read committed: it prevents dirty reads but admits both read skew and write skew. For most operations that is fine; for invariants like “the sum never goes negative” or “exactly one active record”, it is not. The right answer is not “raise everything to serializable” but to write down the list of invariants and close each one — with an index, a lock, or a level.

Is MySQL’s repeatable read the same as snapshot isolation?

Close, but not identical, and the names mean different things in different engines — which is exactly why research uses Adya’s phenomena rather than level names. The practical conclusion is the same: check what outcomes your configuration admits, not what the level is called.

How do we check a real database rather than a teaching engine?

The same way: generate a contended workload, record the history of operations with unique values, and check it for anomalies with a dependency graph. The checker is not tied to any engine — it takes a history in its own format. The mature tool for this is Elle, from the Jepsen project.

Will serializable slow the system down badly?

It does not block more than usual — it aborts more. The cost shows up as the share of transactions you have to retry, and that depends on contention for the same data. So the number cannot be taken from an article: measure it on your own workload, having first made sure the retry exists in the code at all.

Sources

Every claim here can be checked: below are the primary sources, not a retelling.

  1. 01Adya. Weak Consistency: A Generalized Theory and Optimistic ImplementationsMIT thesis, 1999: the G0–G2 phenomena defined through a dependency graph
  2. 02Berenson et al. A Critique of ANSI SQL Isolation LevelsThe 1995 paper that showed the ANSI prose definitions were wrong
  3. 03Cahill, Röhm, Fekete. Serializable Isolation for Snapshot DatabasesThe SSI algorithm implemented in PostgreSQL
  4. 04Elle (Jepsen)The append-only list trick that makes version order recoverable
  5. 05adyaThe engine and checker from this article: MIT, the ladder inside
Author
Doniyor Botirov
Founder of dbit.one · author of this piece

This piece is signed by name, unlike the rest: there is no client behind it. The code it discusses is fully open and published under the same name — you can read it, run it and check the article’s claims instead of taking any of them on trust.

Related services

Read next

Questions about your project?

Describe your task — within 24 hours we’ll come back with an estimate, timeline and plan.

[email protected]