Inside MongoDB Transactions: MVCC, Snapshots, and How WiredTiger Avoids Write Locks

Inside MongoDB Transactions: MVCC, Snapshots, and How WiredTiger Avoids Write Locks

When engineers hear “transactions”, they often assume databases rely heavily on locks.

Row locks. Page locks. Table locks.

But MongoDB’s storage engine WiredTiger takes a different approach.

Instead of locking data during reads and writes, WiredTiger relies on a technique widely used in modern databases:

Multi-Version Concurrency Control (MVCC).

MVCC allows multiple transactions to operate on the same data without blocking each other by maintaining multiple versions of records.

But MongoDB’s design goes further.

It combines:

  • MVCC for transactional consistency
  • snapshot isolation for reads
  • lock-free in-memory updates using skip lists
  • hazard pointers for safe memory access

Understanding how these pieces fit together requires looking at two different layers of the storage engine:

  1. Disk-level transactional storage
  2. In-memory concurrent update structures

The Core Problem

A database must satisfy three competing requirements:

  1. Concurrency
  2. Consistency
  3. Performance

Consider a simple example:


Transaction A:
update balance = 200

Transaction B:
read balance

If B reads while A is updating, several problems may occur:

  • dirty reads
  • inconsistent reads
  • blocked threads
  • lock contention

Traditional databases solve this using locks:


lock(row)
update
unlock(row)

But locks introduce a scalability problem:

  • threads wait
  • CPU cores idle
  • lock queues grow

To avoid this, WiredTiger relies on versioned data instead of locks.


Multi-Version Concurrency Control (MVCC)

MVCC means the database does not overwrite data immediately.

Instead, it creates new versions of records.

Each version is associated with a transaction ID (timestamp).

Example:

KeyValueVersion
user1Atxn 100
user1Btxn 105
user1Ctxn 110

A transaction reads the version that was visible at its snapshot timestamp.

This means:

  • writers do not block readers
  • readers do not block writers
  • transactions see a consistent view of the database

How Snapshot Isolation Works

When a transaction begins, WiredTiger assigns it a read timestamp.

This timestamp defines the snapshot of the database that the transaction will see.

Example timeline:


T1 start → snapshot = 100
T2 writes version 110
T1 reads data

T1 does not see version 110, because it happened after the snapshot.

Instead it reads the latest version ≤ 100.


Snapshot Visibility Rule

A version is visible if:


version_timestamp ≤ transaction_snapshot

Otherwise it is invisible.

This rule guarantees that transactions see a stable snapshot of the database.


Where Versions Are Stored

In WiredTiger, record versions are stored in an update chain attached to each key.

Example:


key: 42

Update chain:

value C (txn 120)
value B (txn 110)
value A (txn 100)

The newest version is at the head of the chain.

Readers traverse the chain until they find a version visible to their snapshot.


Version Selection

Example:

Transaction snapshot = 105

Update chain:


120 -> invisible
110 -> invisible
100 -> visible

The transaction returns value A.

No locks are needed.


How Writes Work

Writes create a new update node.

Example:


old value → B (txn 110)

new write → C (txn 120)

Instead of overwriting the old value, WiredTiger adds a new update node to the chain.

This makes updates extremely cheap because:

  • disk pages are not modified immediately
  • writes only manipulate memory structures

Later, the storage engine merges updates during reconciliation.


Page Reconciliation

Eventually, updates must be persisted to disk.

WiredTiger performs this process called reconciliation.

During reconciliation:

  1. update chains are merged
  2. obsolete versions are removed
  3. a new B+Tree page is written
flowchart LR

Updates --> Reconciliation
Reconciliation --> NewPage
NewPage --> Disk

This process converts multiple in-memory updates into a new immutable disk page.


The Role of Timestamps

WiredTiger transactions rely heavily on timestamps.

Each transaction receives:

  • read timestamp
  • commit timestamp
  • durable timestamp

Example lifecycle:

txn start → read timestamp = 100
updates created
txn commit → commit timestamp = 120

Visibility rules use these timestamps to determine which versions are visible.


Snapshot Reads in WiredTiger

A read operation combines two data sources:

Disk page
+
Update chain

Example:

Disk page:

key 42 → value A

Update chain:

value B (txn 110)
value C (txn 120)

If the transaction snapshot = 115

Visible value = B


Read Flow

sequenceDiagram

participant Client
participant Page
participant UpdateChain

Client->>Page: locate key
Page->>UpdateChain: check updates

UpdateChain->>Client: return visible version

The disk page acts as the base version, while updates represent newer versions.


Why Disk Pages Are Not Updated Directly

Directly modifying disk pages would require:

  • write locks
  • page latching
  • synchronization between threads

Instead WiredTiger follows a copy-on-write model.

Updates accumulate in memory until reconciliation creates a new page.

Benefits:

  • fewer disk writes
  • higher concurrency
  • reduced write amplification

In-Memory Concurrency: Hazard Pointers

The previous sections describe transactional consistency.

But they do not explain how multiple threads safely access memory.

This is where hazard pointers come in.

Hazard pointers solve a different problem:

safe memory reclamation.

When a thread reads a page:

thread.hazard_pointer = page

This signals:

“Do not free this memory while I’m using it.”

When eviction attempts to remove the page:

  1. scan hazard pointers
  2. if referenced → skip eviction

This allows lock-free reads of in-memory pages.


Lock-Free Writes: Skip Lists

Updates are stored in an in-memory skip list.

This structure enables multiple threads to insert updates concurrently.

Insertion uses atomic Compare-And-Swap (CAS).

Example pattern:

while true:
    old = pointer
    new = create_update()

    if CAS(pointer, old, new):
        break

If another thread wins the race, the operation simply retries.

This eliminates the need for write locks.


Putting It All Together

The architecture combines three independent mechanisms.

flowchart TD

Client --> Transaction
Transaction --> Snapshot

Snapshot --> ReadVersion

ReadVersion --> UpdateChain
ReadVersion --> DiskPage

UpdateChain --> SkipList

DiskPage --> Cache

Cache --> HazardPointers
Cache --> Reconciliation

Each layer solves a different problem:

LayerProblem Solved
MVCCtransactional consistency
Snapshot isolationstable reads
Skip listconcurrent updates
Hazard pointerssafe memory access

Disk vs Memory Concurrency Models

Now we can compare the two concurrency strategies used by WiredTiger.


Disk-Level Concurrency (MVCC)

Used for transactional correctness.

Mechanisms:

  • MVCC
  • version chains
  • timestamps
  • snapshot isolation
  • reconciliation

Characteristics:

  • multiple versions of records
  • consistent transaction snapshots
  • readers never block writers

Goal:

correct transactional semantics.


Memory-Level Concurrency (Lock-Free Structures)

Used for CPU scalability.

Mechanisms:

  • hazard pointers
  • lock-free skip lists
  • atomic CAS operations

Characteristics:

  • no read locks
  • no page write locks
  • safe memory access
  • extremely high concurrency

Goal:

maximize throughput on multi-core CPUs.


Key Differences

FeatureDisk LayerMemory Layer
Purposetransactionsconcurrency
TechniqueMVCClock-free algorithms
Data structureB+Tree pagesskip lists
Isolationsnapshot isolationnot transactional
Memory safetyN/Ahazard pointers
Update modelversion chainsCAS pointer updates

Why This Hybrid Design Works

Separating the system into two layers allows WiredTiger to optimize each independently.

Disk layer focuses on:

  • durability
  • transactional correctness
  • efficient storage

Memory layer focuses on:

  • CPU scalability
  • lock-free algorithms
  • low-latency pointer operations

The result is a system that achieves both:

  • strong transactional guarantees
  • extremely high concurrency

Key Takeaways

Modern storage engines separate transactional logic from concurrency control.

WiredTiger achieves this using a layered design:

  • MVCC provides transactional consistency
  • snapshot isolation guarantees stable reads
  • update chains maintain multiple versions
  • skip lists enable lock-free writes
  • hazard pointers protect in-memory pages

This architecture allows MongoDB to support:

  • high transaction throughput
  • massive parallelism
  • minimal lock contention

Final Thoughts

The most interesting part of WiredTiger’s design is how it combines classic database ideas with modern lock-free techniques.

Traditional databases focused heavily on:

  • locks
  • latches
  • serialized writes

WiredTiger instead embraces:

  • versioned data
  • optimistic concurrency
  • atomic pointer updates

The result is a storage engine that scales across modern multi-core systems while still providing the transactional guarantees developers expect.

Understanding this layered architecture provides a deeper appreciation of how modern databases achieve both consistency and extreme concurrency.