Read Skew Explained: Inconsistent Snapshots Across Multiple Objects
Why Read Skew Is Not the Same as a Non-Repeatable Read
Read skew occurs when a transaction reads two logically related objects at different points in time β one before and one after a concurrent transaction commits β producing a view that never existed as a committed whole.
π― System Scenario & Requirements
System Goal
Read skew occurs when a transaction reads two logically related objects at different points in time β one before and one after a concurrent transaction commits β producing a view that never existed as a committed whole.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: Read skew occurs when a transaction reads two logically related objects at different points in time β one before and one after a concurrent transaction commits β producing a view that never existed as a committedβ¦
- Read Committed isolation cannot prevent it because each SQL statement gets a fresh snapshot.
- Snapshot isolation (PostgreSQL's REPEATABLE READ , CockroachDB's default SERIALIZABLE ) fixes it by giving every transaction a single consistent point-in-time view for its entire duration.
Constraints & Assumptions
- Meridian's overnight transfer service moved $200 from checking to savings accounts at high volume.
- The service had been written without a single wrapping database transaction: it performed two sequential, auto-committing SQL updates.
- Prevention is not one-size-fits-all.
- The right strategy depends on your workload's consistency requirements, your tolerance for retry overhead, and whether you own the write side, the read side, or both.
β‘ Execution Stages & Decision Trace
Stage 1 β How a Transfer Produces Two Incompatible Snapshots
The sequence diagram below traces the exact interaction that caused Meridian's outage.
T1 is the transfer service performing a non-atomic debit-then-credit.
Deep Dive
Scenario context
Read skew occurs when a transaction reads two logically related objects at different points in time β one before and one after a concurrent transaction commits β producing a view that never existed as a committed whole. Read Committed isolation cannot prevent it because each SQL statement gets a fresh snapshot. Snapshot isolation (PostgreSQL's REPEATABLE READ, CockroachDB's default SERIALIZABLE) fixes it by giviβ¦
This walkthrough focuses on Why Read Skew Is Not the Same as a Non-Repeatable Read.
Before making a design choice, clarify:
- Both anomalies are about reads returning unexpected values, which makes them easy to conflate.
- The distinction is precise and matters enormously for choosing a fix.
- A non-repeatable read involves re-reading the same single row within a transaction and getting a different value.
Concepts in play: database-isolation-levels-anomalies, consistency-replication, git-internals-and-versioning
Stage 1 β How a Transfer Produces Two Incompatible Snapshots
- The sequence diagram below traces the exact interaction that caused Meridian's outage.
- T1 is the transfer service performing a non-atomic debit-then-credit.
- T2 uses READ COMMITTED isolation, which means each of its SELECT statements takes an independent snapshot of the currently-committed database state.
- The diagram shows the invariant window : the gap between when T1's debit becomes visible and when T1's credit completes.
sequenceDiagram
participant T1 as Transfer Service
participant DB as Accounts Database
participant T2 as Risk Engine Reader
T1->>DB: Debit checking. Set balance 500 to 300. COMMIT.
Note over DB: Checking=300 committed. Savings=500 unchanged. Invariant window open.
T2->>DB: BEGIN
T2->>DB: SELECT checking account balance
DB-->>T2: 300
T1->>DB: Credit savings. Set balance 500 to 700. Not yet committed.
T2->>DB: SELECT savings account balance
DB-->>T2: 500. Savings credit not yet committed.
T1->>DB: COMMIT savings credit
Note over T2: Computed total = 300 + 500 = 800. Invariant expects 1000.
Observable outcome: The diagram shows the invariant window : the gap between when T1's debit becomes visible and when T1's credit completes.
Stage 2 β MVCC and the Consistent Snapshot: How Snapshot Isolation Ends the Straddle
- Multi-Version Concurrency Control (MVCC) is the mechanism that makes snapshot isolation practical at scale without needing read locks.
- Every major database β PostgreSQL, Oracle, MySQL InnoDB, CockroachDB, and virtually all cloud-native databases β maintains multiple committed versions of each row.
- Each version is tagged with the transaction ID (or timestamp) of the transaction that wrote it.
- The Internals of MVCC Row Versioning When a row is updated, the database does not overwrite the existing value.
sequenceDiagram
participant T1 as Transaction 1
participant DB as Database
participant T2 as Transaction 2
T1->>DB: Read X (value=100)
T2->>DB: Read X (value=100)
T1->>DB: Write X = 50
T2->>DB: Write X = 75
DB->>DB: Conflict!
Observable outcome: The Internals of MVCC Row Versioning When a row is updated, the database does not overwrite the existing value.
Stage 3 β Which Isolation Level Stops Read Skew: The Complete Anomaly Prevention Map
- The ANSI SQL standard defines four isolation levels.
- Most production databases implement an additional level β Snapshot Isolation β which sits between Repeatable Read and Serializable in terms of strength.
- The table below maps each level to the anomalies it prevents or allows.
- Three observations critical for system design decisions: Snapshot Isolation is the minimum viable fix for read skew.
Observable outcome: Three observations critical for system design decisions: Snapshot Isolation is the minimum viable fix for read skew.
Stage 4 β The Multi-Service Read Path Where Inconsistency Enters the System
- In a monolithic application backed by a single database, read skew is a database-level concern: choose the right isolation level and it disappears.
- In a microservices architecture, the attack surface is larger.
- Services may query different databases, read from different replicas, or call independent services that each apply their own isolation rules.
- No single database transaction can span a service boundary.
graph TD
TransferSvc[Transfer Service] -->|debit committed| AccDB[(Accounts Database)]
TransferSvc -->|credit pending| AccDB
RiskEngine[Risk Engine] -->|reads checking 300| AccDB
RiskEngine -->|reads savings 500 stale| AccDB
RiskEngine --> Calc[Computed total 800]
Calc -->|expected 1000 discrepancy| AlertSvc[Alert Service]
AlertSvc --> FreezeOp[Account Freeze]
Observable outcome: No single database transaction can span a service boundary.
Stage 5 β How PostgreSQL, Oracle, MySQL InnoDB, and CockroachDB Handle Read Skew
- Understanding how each database names, implements, and defaults its isolation levels is essential for knowing what "fixing" read skew actually requires in your stack.
- PostgreSQL Default: Read Committed.
- Read skew is possible with the default.
- Setting REPEATABLE READ activates true Snapshot Isolation β the transaction receives a consistent point-in-time view from BEGIN to COMMIT .
Observable outcome: Setting REPEATABLE READ activates true Snapshot Isolation β the transaction receives a consistent point-in-time view from BEGIN to COMMIT .
Stage 6 β Multi-Datacenter Amplification: How Replica Lag Turns a Race Into a Certainty
- In a single-datacenter deployment, read skew requires unfortunate timing: the reader's two queries must straddle a writer's commit window.
- With sub-millisecond inter-node latency, this window may be microseconds wide.
- The anomaly is possible but not guaranteed during any given request.
- In a geographically distributed deployment with multi-region asynchronous replication, read skew stops being a timing race and becomes a structural property of the system.
Observable outcome: In a geographically distributed deployment with multi-region asynchronous replication, read skew stops being a timing race and becomes a structural property of the system.
End-to-end outcome
After working through the stages, you should be able to explain:
- "Read Committed" is a misleading name for invariant-crossing reads.
- The label sounds safe β you are only reading committed data.
- Read skew is a cross-object inconsistency : a transaction assembles two logically related values from different committed states, producing a view that never existed as a whole at any single moment in time.
- The root cause is statement-level snapshots : Read Committed gives each SQL statement its own fresh snapshot, allowing a multi-statement transaction to straddle a concurrent writer's commit window.
Alternatives considered
The source focuses on one primary design. A second architecture is not compared, so no alternative is invented here.
Failure and degraded modes
The Meridian Bank Failure: What Read Skew Looks Like in Production
- Detection: Meridian's overnight transfer service moved $200 from checking to savings accounts at high volume.
- User or operator impact: Meridian's overnight transfer service moved $200 from checking to savings accounts at high volume.
Prevention Strategies and Their Trade-offs
- Detection: Prevention is not one-size-fits-all.
- User or operator impact: Prevention is not one-size-fits-all.
