The Phantom Read Problem: Why Repeatable Read Isn't as Repeatable as It Sounds

2026-06-14

You set your transaction isolation to REPEATABLE READ thinking you're safe. You run the same query twice in one transaction and get different row counts. What happened? A phantom read: another transaction inserted (or deleted) rows that match your WHERE clause between your two reads. Repeatable Read protects rows you've already seen from being modified — it doesn't stop new rows from appearing.

The SQL standard defines four isolation levels by which anomalies they prevent:

Non-repeatable read vs phantom read: non-repeatable is the same row changing between reads (UPDATE). Phantom is the set of matching rows changing (INSERT/DELETE).

Real-world example. A bank runs end-of-day interest calculation:

Under standard Repeatable Read, this is legal. The fix depends on your database:

Rule of thumb. If your transaction makes a decision based on the absence of rows ("no account exists with this email, so insert one") or aggregates over a set ("sum all matching rows"), Repeatable Read is not enough. Use Serializable, or enforce the invariant with a unique constraint, advisory lock, or SELECT ... FOR UPDATE on a sentinel row.

Cost trade-off. Serializable on Postgres uses SSI (Serializable Snapshot Isolation), which adds ~10-30% overhead and can abort transactions with serialization failures. Your code must retry. Read Committed is cheap but lets anomalies through. Pick per-transaction, not per-database.

See it in action: Check out How to fix valorant audio #valorant #gaming by ByJackfrost to see this theory applied.
Key Takeaway: Repeatable Read prevents the rows you've seen from changing, but new rows matching your query can still appear — if your logic depends on the absence or count of rows, you need Serializable or explicit locking.

All newsletters