MongoDB ACID — Part 3: Isolation — Snapshot Isolation and concurrency
Isolation in MongoDB is built from MVCC and Read Concern. This Part 3 walks through dirty reads, non-repeatable reads, phantoms, and read skew without forcing SQL isolation labels onto MongoDB, and clarifies what multi-document transactions with snapshot read concern actually guarantee. It connects fail-on-conflict behavior, client retries, and lock-acquisition timeouts into one mental model, compares local, majority, and snapshot read concerns with replication caveats, spells out prerequisites for causal consistency, and discusses write skew mitigations with their operational trade-offs. Part 4 will continue with durability and Write Concern.
Series outline
- Part 1 — ACID concepts + MongoDB’s historical context
- Part 2 — Atomicity & Consistency in depth: single document vs multi-document
- Part 3 — Isolation and snapshot isolation internals (this post)
- Part 4 — Durability, WiredTiger, Write Concern (forthcoming)
- Part 5 — Production patterns, performance, anti-patterns (forthcoming)
Table of contents
- Introduction
- Four isolation anomalies
- SQL isolation levels and MongoDB — why the matrix misleads
- MVCC — how MongoDB handles concurrency
- Snapshot isolation — where guarantees apply
- Read Concern — tuning read consistency
- Write conflicts: fail-on-conflict vs lock acquisition timeout
- Causal consistency — read the prerequisites
- Practical sketch — read concern and transactions
- Limits — write skew and mitigations
- Closing
1. Introduction
Among the four ACID properties, Isolation is the easiest to underestimate — and the richest source of “it worked in staging” stories.
Part 2 framed Atomicity and Consistency and told you to read readConcern and writeConcern together. This post narrows the lens to Isolation—what concurrent transactions can and cannot observe—and to read concern as the knob for read consistency. Write concern as a durability / replication lever returns in Part 4.
You might ask, “As long as I read committed data, I’m fine, right?” In practice, isolation is about which committed snapshot you read, and whether all reads in a workflow see the same one. A compact summary of MongoDB’s model is:
MVCC-based snapshot isolation — on conflict, fail fast and retry instead of waiting forever.
This article unpacks that line and separates what holds inside a multi-document transaction from what holds for ordinary single operations.
2. Four isolation anomalies
When transactions overlap, textbooks cite four patterns:
2.1 Dirty read
Reading data another transaction has not yet committed. If that transaction rolls back, your read never corresponded to a durable fact.
2.2 Non-repeatable read
Inside one transaction, two reads with the same predicate return different results because another transaction committed in between.
2.3 Phantom read
The set of matching rows/documents changes between reads in the same transaction (inserts/deletes).
2.4 Read skew
You read related values at skewed logical times — for example account A and B balances — and the combination is impossible in any real committed state.
These definitions are the ladder for everything that follows about snapshot reads and Read Concern.
3. SQL isolation levels and MongoDB — why the matrix misleads
The SQL standard describes isolation levels tuned for locking implementations. MongoDB uses WiredTiger MVCC, so a claim like “MongoDB equals REPEATABLE READ” is often more confusing than helpful.
Practical advice:
- Do not treat the SQL table as MongoDB’s final answer.
- Read Read Concern, whether you are in a transaction, and deployment topology (standalone / replica set / sharded) together.
3.1 Do not equate local with READ UNCOMMITTED
Blog posts sometimes label local “close to READ UNCOMMITTED.” Readers then import the SQL chart and imagine dirty reads as the headline risk.
In MongoDB, local is better read as the freshest data visible on the member you are connected to, plus replication caveats: during failovers, data you observed may roll back. That is closer to node visibility + durability trade-offs than to a one-to-one SQL label.
3.2 When people say “snapshots eliminate phantoms”
Inside a multi-document transaction with readConcern: "snapshot" (the transaction default), reads share one consistent snapshot. The guarantee attaches to that transaction boundary.
If you read outside a transaction, or you mix concerns across calls, you can still observe different logical times. Saying “snapshots erase every phantom everywhere” without that context over-promises. Name transaction + session + read concern whenever you make a strong claim.
4. MVCC — how MongoDB handles concurrency
Snapshot isolation assumes Multi-Version Concurrency Control (MVCC).
4.1 Core idea
Instead of overwriting a single row in place, writers create new versions; readers choose the version visible to them. That reduces unnecessary blocking between readers and writers.
4.2 WiredTiger and snapshots
WiredTiger decides visibility using transaction ids. A transaction’s snapshot determines which committed writes are visible, which is how uncommitted writes from other transactions stay invisible to you.
5. Snapshot isolation — where guarantees apply
The heart of snapshot isolation:
Reads inside one transaction behave against a single, stable snapshot.
While your transaction is open, other transactions may commit — yet your reads stay pinned to the snapshot you started with, which is why snapshot reads help with read skew inside that transaction.
5.1 Relation to anomalies (high level)
With snapshot read concern in a typical multi-document transaction, you usually reason about strong protection against dirty reads, non-repeatable reads inside the transaction, and read skew — inside that transaction. Write skew is different; see §10.
6. Read Concern — tuning read consistency
6.1 local
- Returns the most recent data visible on the member.
- Fast, but combined with replication, you must remember rollback windows after elections.
- Mission-critical financial designs rarely stop at “default
localonly” without a conscious review.
6.2 majority
- Reads data acknowledged by a majority of replica-set members — a more conservative bar than
localfor “will not roll back” reads in the usual replication story.
6.3 snapshot (multi-document transactions)
- Provides a consistent snapshot across the transaction — the anchor for coordinated reads.
6.4 linearizable
- Very strong, higher latency, narrow use cases (often single-document reads in documentation — verify your version).
6.5 Transaction-level options, not per-operation knobs
In multi-document transactions, you generally set Read Concern on startTransaction / withTransaction, not by hoping a per-find option overrides the transaction. Confirm details for your driver version.
await session.withTransaction(
async () => {
const doc = await collection.findOne({ _id: 1 }, { session });
// ...
},
{
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
}
);
7. Write conflicts: fail-on-conflict vs lock acquisition timeout
Lock-heavy databases often block writers until the winner finishes. MongoDB’s document-level path is closer to optimistic conflict detection: conflicting writers fail quickly so the application can retry — the usual “fail-on-conflict” story.
7.1 Why avoid unbounded waiting
Unbounded waiting invites deadlock management costs. MongoDB prefers to surface conflicts early and push bounded retries to the client.
7.2 withTransaction and retries
session.withTransaction() can retry on transient errors such as those labeled TransientTransactionError. Production code should still cap retries, backoff, and preserve idempotency.
7.3 Not the same story as maxTransactionLockRequestTimeoutMillis
Mixing these causes fake contradictions:
- Write conflict: another transaction already touched the data you need; you typically retry the transaction.
- Lock acquisition timeout: you waited briefly to acquire a lock and timed out — see
maxTransactionLockRequestTimeoutMillisin the manual (defaults vary by version; read the parameter docs).
So “Mongo doesn’t wait” and “there is a short lock wait” describe different layers.
8. Causal consistency — read the prerequisites
Causal consistency is about reading your own writes and related causal order in a session — but it is not “turn on a flag and every secondary magically catches up.”
Checklist:
- Reuse the same client session where required.
- Align read and write concern with what causal guarantees need (often
majoritywrites matter). - If read preference targets secondaries, accept replication lag or design around it.
If reads use local while writes use majority without a coherent plan, your “just wrote then read” story can still break. Follow the causal consistency chapter for your server and driver versions.
const session = client.startSession({ causalConsistency: true });
await collection.updateOne(
{ _id: userId },
{ $set: { city: "Seoul" } },
{ session, writeConcern: { w: "majority" } }
);
const user = await collection.findOne(
{ _id: userId },
{ session, readConcern: { level: "majority" } }
);
9. Practical sketch — read concern and transactions
Imagine aggregating balances while a transfer transaction is mid-flight. If you must not see a half-applied transfer, a single local aggregation may be the wrong tool.
const session = client.startSession();
let total;
await session.withTransaction(
async () => {
const agg = await accounts
.aggregate(
[{ $group: { _id: null, total: { $sum: "$balance" } } }],
{ session }
)
.toArray();
total = agg[0]?.total;
},
{ readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } }
);
You are asking for a transactional snapshot of the aggregate. Splitting reads with local across calls can stitch together different time slices.
10. Limits — write skew and mitigations
10.1 Write skew
Two transactions each read a snapshot, update different documents, and together violate a global rule (classic “at least one doctor on call”). Snapshot isolation does not automatically prevent that class of bug.
10.2 Dummy updates and trade-offs
A common pattern forces conflict by touching a shared document (sometimes via a _lastChecked field). That can work but adds audit noise, write amplification, and index pressure.
Alternatives:
- Encode invariants in one atomic update where possible.
- Use unique indexes / partial indexes to shrink the state space.
- Remodel to reduce contention hot spots.
10.3 Do not mix read concerns inside one transaction
Design at transaction scope; avoid imagining per-operation overrides.
11. Closing
| Theme | Takeaway |
|---|---|
| MVCC | Versioned data reduces reader/writer blocking. |
| Snapshot isolation | Consistent reads inside a transaction’s snapshot. |
| fail-on-conflict | Surface conflicts early; pair with retries. |
| Read Concern | Changes what “read” means — local / majority / snapshot. |
| Write skew | May need modeling and constraints beyond snapshots alone. |
| Causal consistency | Needs aligned session, concerns, and read preferences. |
Part 4 continues with durability, WiredTiger journaling/checkpoints, and Write Concern under failure.
References
- MongoDB Manual — Transactions (in-progress transactions and write conflicts)
- MongoDB Manual — Read Concern
- MongoDB Manual — Read Concern "snapshot"
- MongoDB Manual — maxTransactionLockRequestTimeoutMillis
- MongoDB Manual — Causal Consistency
- MongoDB Manual — Transactions in Applications (retries,
TransientTransactionError, write-skew patterns) - MongoDB Manual — Atomicity of single-document writes
Written April 2026. Defaults, driver behavior, and parameter meanings change across versions — cite the manual for the build you run.