Lock-Free Concurrency in MongoDB: Hazard Pointers and Skip Lists Inside WiredTiger
Lock-Free Concurrency in MongoDB: Hazard Pointers and Skip Lists Inside WiredTiger
Most engineers assume that databases rely heavily on locks to coordinate concurrent access.
But locks don’t scale well.
As CPU core counts increase, lock contention becomes one of the biggest bottlenecks in modern database systems. Threads block, CPUs idle, and throughput collapses under high concurrency.
MongoDB’s default storage engine WiredTiger takes a different approach.
Instead of relying primarily on locks, it uses lock-free algorithms for both reads and writes.
Two key mechanisms make this possible:
- Hazard Pointers → Lock-free reads
- Skip Lists + CAS → Lock-free writes
Together, these techniques allow WiredTiger to handle massive concurrency while fully utilizing modern multi-core hardware.
The Problem
Modern databases must support:
- thousands of concurrent client connections
- millions of reads and writes per second
- efficient use of multi-core CPUs
Traditional storage engines rely on locking to maintain consistency.
Example:
lock(page)
read(page)
unlock(page)
Even shared locks introduce coordination overhead:
- threads must synchronize
- cache lines bounce between CPU cores
- lock queues form under contention
As concurrency increases, lock-based systems often hit scalability limits.
To avoid this, WiredTiger uses lock-free data structures.
The Role of the Storage Engine
In MongoDB, the storage engine is responsible for:
- persisting documents on disk
- managing the in-memory cache
- coordinating concurrent reads and writes
- ensuring durability and crash recovery
MongoDB’s default storage engine is WiredTiger.
On disk, WiredTiger stores data using a B+Tree.
B+Trees are ideal for databases because they support:
- fast point lookups
- efficient range scans
- balanced performance for reads and writes
- disk-friendly page layouts
But the in-memory representation is very different from the on-disk structure.
This separation enables powerful concurrency optimizations.
Disk vs Memory Representation
WiredTiger maintains two different representations of data.
| Layer | Structure | Purpose |
|---|---|---|
| Disk | B+Tree pages | Durable storage |
| Memory | Cached pages + update structures | Concurrent access |
Disk structures prioritize:
- compression
- sequential I/O
- storage efficiency
Memory structures prioritize:
- concurrency
- fast pointer manipulation
- lock-free algorithms
This design allows WiredTiger to use in-memory structures that would be impractical for disk storage.
Lock-Free Reads Using Hazard Pointers
When a thread reads a document, it typically accesses a page cached in memory.
However, the cache manager may evict pages to free memory.
If a page were evicted while a thread was reading it, the thread might dereference a dangling pointer, causing crashes or memory corruption.
Traditional databases solve this using read locks.
But WiredTiger uses hazard pointers instead.
What Is a Hazard Pointer?
A hazard pointer allows a thread to announce which memory object it is currently accessing.
Before accessing a page, the thread publishes the pointer in a hazard slot.
thread.hazard_pointer = page
memory_barrier()
After the read completes:
thread.hazard_pointer = NULL
This simple mechanism tells the system:
“This thread is currently using this memory object.”
How Eviction Works
When the cache manager wants to evict a page:
- It scans hazard pointers from all threads.
- If any thread references the page → eviction is skipped.
- If no hazard pointer references it → the page can be safely evicted.
This guarantees:
- no dangling pointers
- no read locks
- minimal synchronization overhead
Lock-Free Writes Using Skip Lists
Reads are only half the story.
Databases must also support highly concurrent writes.
Traditional systems use page-level write locks:
lock(page)
modify data
unlock(page)
This serializes updates and limits scalability.
Instead, WiredTiger stores in-memory updates using a lock-free skip list.
Why Skip Lists?
A skip list is a probabilistic data structure similar to a balanced tree.
Conceptually it looks like multiple linked lists stacked on top of each other.
Level 3: 30 -------- 70
Level 2: 30 ---- 50 ---- 70
Level 1: 10-20-30-40-50-60-70
Higher levels act as express lanes, allowing faster traversal.
Skip lists provide:
- O(log n) search time
- simple pointer manipulation
- natural support for lock-free algorithms
Atomic Compare-And-Swap (CAS)
Updates in the skip list rely on an atomic CPU instruction called Compare-And-Swap (CAS).
CAS works like this:
if (*pointer == expected_value)
*pointer = new_value
The update succeeds only if the pointer hasn’t changed.
If another thread modified it first, the operation fails and the thread retries.
Example pattern:
while(true) {
old = pointer
new = build_update(old)
if CAS(pointer, old, new)
break
}
This technique is called optimistic concurrency.
Instead of locking, threads simply retry if conflicts occur.
Why Not Doubly Linked Lists?
Lock-free algorithms depend on atomic pointer updates.
A singly linked list insertion requires only one pointer change:
CAS(A.next, B, X)
But a doubly linked list requires two updates:
A.next = X
B.prev = X
These operations cannot be performed atomically with a single CAS instruction.
If a thread crashes or gets interrupted between updates, the structure becomes inconsistent.
Skip lists avoid this problem because they rely on singly linked forward pointers, which can be safely updated using CAS.
How Reads Combine Disk Data and Updates
Another important design choice:
WiredTiger does not modify disk pages directly during writes.
Instead, updates are stored in memory.
A read operation merges:
disk page
+
in-memory updates
Example:
Disk page:
key: 20 → value A
Update chain:
key:20 → value B
key:20 → value C
The reader applies the newest update to return the correct value.
This allows updates to occur without modifying the base page immediately.
Example Write Scenario
A simplified write path looks like this:
- Locate the leaf page in the B+Tree.
- Create an update node.
- Insert the update into the skip list using CAS.
- Commit the transaction.
No disk write occurs at this stage.
Later, WiredTiger performs page reconciliation, merging updates into a new B+Tree page and writing it to disk.
Internal Architecture
The interaction between memory structures and disk storage looks like this:
flowchart TD
Client --> Read
Client --> Write
Read --> CachedPage
Write --> UpdateNode
UpdateNode --> SkipList
SkipList --> CachedPage
CachedPage --> HazardPointer
CachedPage --> Reconciliation
Reconciliation --> DiskPage
DiskPage --> BPlusTree
Example Concurrency Scenario
The following sequence shows how concurrent reads and writes operate safely.
sequenceDiagram
participant Reader
participant Cache
participant SkipList
participant Eviction
Reader->>Cache: Access page
Reader->>Cache: Publish hazard pointer
SkipList->>Cache: Insert update using CAS
Eviction->>Cache: Attempt page eviction
Cache->>Eviction: Page protected by hazard pointer
Eviction->>Cache: Skip eviction
Reader->>Cache: Read merged result
Reader->>Cache: Clear hazard pointer
Readers never block writers, and writers never block readers.
Why This Architecture Works
This hybrid architecture gives WiredTiger several advantages.
Lock-Free Reads
Hazard pointers eliminate read locks and prevent unsafe memory reclamation.
Lock-Free Writes
CAS-based skip list updates allow concurrent writes without page locks.
CPU Scalability
Threads rarely block each other, allowing efficient multi-core scaling.
Efficient Disk Writes
Updates accumulate in memory and are later merged during reconciliation.
Key Takeaways
Modern database engines must scale across dozens of CPU cores.
Traditional lock-heavy designs struggle under high contention.
WiredTiger solves this using lock-free techniques:
- Hazard pointers protect in-memory pages during reads
- Skip lists + CAS enable concurrent lock-free updates
- Separate memory and disk structures optimize both performance and durability
- Deferred page reconciliation reduces disk write amplification
These design choices allow MongoDB to achieve high throughput and excellent concurrency on modern hardware.
Final Thoughts
Lock-free algorithms are notoriously difficult to implement correctly.
They require careful reasoning about:
- memory ordering
- atomic operations
- pointer safety
- race conditions
But when done right, the payoff is enormous.
WiredTiger demonstrates how combining hazard pointers, CAS-based structures, and careful separation of memory and disk layers can unlock massive concurrency in modern database systems.
For engineers exploring database internals, these techniques offer a fascinating glimpse into how high-performance storage engines are built.