LevelDB is Google's distilled storage engine (Jeff Dean & Sanjay Ghemawat, the Bigtable tablet lineage): an ordered key-value store built entirely from append-only structures — a WAL, an in-memory skip list, and immutable sorted files (SSTables) organized in levels and merged by background compaction. It is the smallest complete implementation of the write-optimized (LSM) answer to B-trees, small enough to read end-to-end in a week, and it begat RocksDB and a decade of storage engines. This deep-dive teaches the fundamental trade: sequential writes + amortized reorganization vs read amplification, with almost no incidental complexity around it.
🧒 Explain like I'm 10
A diary, a scratch page, and a tidy-up robot. When you tell LevelDB
something (Put), it does two fast things: writes the fact in pen in a
diary you never re-read (the WAL — its only job is to survive a crash)
and scribbles it on a scratch page (the memtable). When the scratch page
fills up (~4MB), it is copied neatly into a labeled folder (an immutable
SSTable) and filed into a drawer, and folders from fuller drawers keep
getting merged into bigger, tidier folders by a tidy-up robot
(compaction) that works in the background. Each folder wears a magic
sticker (a Bloom filter) that says "this key is DEFINITELY not in here,"
so a lookup can skip most drawers without opening them.
Why the grown-ups did it this way: writing in pen at the back of a notebook is much faster than erasing and rewriting sorted pages in place — so writes stay blazing fast, and the tidying happens later, a little at a time, off the critical path.
1. Architecture overview
flowchart TD
subgraph write["write path — single writer at a time, group commit"]
PUT["Put key, val — DBImpl Write (db_impl.cc 1206)"]
WAL["WAL append — db/log_writer.cc<br/>32KB blocks: crc / length / type"]
MEM["memtable — skiplist keyed by user key, seq<br/>db/memtable.cc"]
IMMF["mem frozen to imm, new mem + new WAL"]
BT["BuildTable — db/builder.cc 17<br/>write immutable SSTable to level 0"]
end
subgraph bg["background"]
COMP["BackgroundCompaction 708 —<br/>merge SSTables level by level<br/>k-way merge, drops shadowed versions"]
MAN["MANIFEST — log of VersionEdits;<br/>CURRENT names the live manifest"]
end
subgraph read["read path — Get (db_impl.cc 1121)"]
R1["mem → imm → levels 0..N<br/>(newest → oldest)"]
R2["Version (version_set.cc 324) → Table<br/>+ Bloom filters skip whole files"]
end
PUT --> WAL
PUT --> MEM
MEM -->|"~4MB: MakeRoomForWrite 1331"| IMMF --> BT
BT --> COMP
COMP --> MAN
R1 --> R2
R2 -.->|"reads immutable files"| BT
Diagram source — enable JavaScript to render the diagram.- Two threads of control: the foreground (user calls: Get/Put/Write)
and one background compaction thread, coordinated by one mutex and
condition variables (
db/db_impl.cc:MaybeScheduleCompaction, line 668). - The spine — one
Put— is:DBImpl::Write→ WAL record → memtable insert → (maybe)MakeRoomForWrite(line 1331) freezes/flushes.
Walkthrough: one Put, through crash and back out of Get
The complete life of an LSM record — step through it on the site, or read it straight down.
sequenceDiagram
autonumber
participant C as Caller (Put)
participant W as DBImpl Write 1206
participant L as WAL (log_writer.cc)
participant M as memtable skiplist
participant BG as compaction thread
participant D as disk (SSTables + MANIFEST)
C->>W: Put key, val
W->>L: append record (crc, block-framed) — mutex released
W->>M: insert (user_key, seq) — newest sorts first
Note over W,M: durable here; readable by any Get that starts now
M->>D: at ~4MB: freeze mem, BuildTable → L0 SSTable
BG->>D: merge levels, install via MANIFEST VersionEdit
Note over BG,D: crash? reopen = replay WAL + replay MANIFEST
C->>W: later: Get key
W->>M: mem → imm → levels, Bloom filters skip files
Diagram source — enable JavaScript to render the diagram.One writer, many passengers
DBImpl::Write (1206) parks
the caller in a writer queue; the front writer becomes the serialization
point and BuildBatchGroup merges everyone waiting into one batch —
group commit for free.
The pen stroke
The batch is appended to the WAL: 32KB
blocks of <crc | length | type> with FULL/FIRST/MIDDLE/LAST framing
(db/log_format.h). A torn write can corrupt at most one block; the
reader resyncs at the next block boundary. The mutex is released
during this append and the memtable insert — the slow parts run without
the lock.
Scribble on the scratch page
Each key/value lands in the
skiplist as (user_key, sequence_number) with the newest version
sorting first (db/dbformat.h) — lookups always see the latest value,
and this is what makes snapshots cheap later.
The page fills
At ~4MB, MakeRoomForWrite (1331) freezes
the memtable (mem_ becomes imm_), starts a fresh one, and hands the
frozen page to BuildTable (db/builder.cc:17): iterate in order, emit
prefix-compressed blocks, an index entry per block, a Bloom filter, and
the fixed footer — an immutable file appears.
The robot tidies
The single background thread merges
levels (BackgroundCompaction, 708): a k-way merge re-sorts runs,
drops versions shadowed below the oldest snapshot (910–971), and
installs the new layout by appending a VersionEdit to the MANIFEST
(VersionSet::LogAndApply, 777). Files are only deleted after that
edit is durable.
Crash, restart, replay
On reopen, VersionSet::Recover
(861) replays MANIFEST edits to rebuild the layout, and the WAL is
replayed into a fresh memtable — stopping at the first bad crc. An
orphan SSTable from a crashed flush is adopted or deleted, never
half-visible, because installation is the manifest write.
Read it back
DBImpl::Get (1121) takes a snapshot seq,
then walks mem → imm → levels via Version::Get (324); Bloom filters
skip whole files, and the first version found (newest wins) is the
answer.
Append, merge, never rewrite
The key was durable at step 2, searchable at step 3, and ordered-by-merge forever after — the LSM bargain: fast writes today, amortized sorting later.
2. Core data structures — and why these
MemTable = arena + skip list (db/memtable.cc, db/skiplist.h)
The skiplist (db/skiplist.h:40, kMaxHeight = 12, kBranching = 4
at lines 100/242) keys entries by (user_key, sequence_number) with
descending sequence, so the newest version sorts first and lookups see
the latest value. Memory comes from an Arena (util/arena.h:16) —
bump allocation, freed wholesale; the skip list needs no per-node
dealloc because the memtable lives and dies as a unit. Why a skip list
not a red-black tree: lock-free-friendly, simpler code, and — crucial
for a structure dumped to a sorted file — iteration order falls out of
the same descent as search.
The WAL record format (db/log_format.h, db/log_writer.cc)
Records are 32KB blocks of <4-byte crc | 2-byte length | 1-byte type>
(kHeaderSize at log_format.h:30) with types FULL/FIRST/MIDDLE/LAST
(enum RecordType, line 14) for splitting large batches across blocks.
Why blocks: a torn write at crash time corrupts at most one block, and
the reader resyncs at the next block boundary — recovery reads the log
and stops at the first bad crc. The log is the durability boundary;
everything after it is a cache.
SSTables (table/)
A table is: data blocks (prefix-compressed key/value chunks) + an
optional filter block (Bloom) + an index block (one entry per data
block, its key ≥ all keys in that block) + a fixed footer with the
index/metaindex handles and magic number (table/format.cc:Footer::EncodeTo,
line 32). Reads use a two-level iterator (table/two_level_iterator.cc:18):
index block → data block. Why immutable sorted files: write once, then
mmap/read-only forever — no torn structures, trivially shareable
between readers and the compactor, cheap crc verification per block.
Version / VersionSet / MANIFEST (db/version_set.cc)
The current layout (which files, which levels, which compaction
pointers) is itself an append-only log: every compaction appends a
VersionEdit to the MANIFEST; CURRENT names the live manifest;
VersionSet::Recover (line 861) replays edits at startup. Why: the file
layout is mutable state, and LevelDB refuses special treatment — mutable
state gets the same log+replay discipline as user data
(VersionSet::LogAndApply, line 777).
3. Concurrency and failure model
- One foreground mutex guards everything mutable; the big lesson is
what runs outside it: WAL append + memtable insert happen with
mutex_.Unlock()(seeDBImpl::Write, db_impl.cc:1206-1258 — a single front-of-queue writer acts as the serialization point for the whole batch group, so the lock is dropped during the slow parts). - Group commit: queued writers are merged by
BuildBatchGroupinto one WAL record — batching under load for free. - Snapshots are sequence numbers (
db/snapshot.h): reads pin a seq; compaction must retain versions abovesmallest_snapshot(db/db_impl.cc:910-971) — MVCC with two dozen lines, because the key already carries the version. - Failure/recovery: on open, replay WAL into a fresh memtable
(
DBImpl::Recover); if the MANIFEST is torn, its log format means replay stops at the last complete edit. A crash mid-flush leaves an orphan SSTable that is either adopted (via manifest edit) or deleted — never half-visible, because installation is the manifest write. - Overload = backpressure, not errors: too many L0 files → writes
slow down (
kL0_SlowdownWritesTrigger, db_impl.cc:1342), then stall (kL0_StopWritesTrigger, line 1362) until compaction drains —MakeRoomForWrite(1331) is where the engine chooses delay over unbounded memory.
flowchart LR
subgraph fg["foreground — user calls"]
MUTEX["one mutex guards mutable state"]
WR["writer queue — front writer serializes<br/>the batch group; lock released during WAL + insert"]
end
subgraph snaps["snapshots"]
SN["a snapshot = a sequence number<br/>compaction keeps versions above<br/>smallest_snapshot (910-971)"]
end
subgraph bgd["background — one thread"]
BGT["flush + compaction share it (the honest wart)<br/>coordinated by MaybeScheduleCompaction 668"]
end
subgraph overload["overload = backpressure, not errors"]
SLOW["kL0 SlowdownWritesTrigger 1342<br/>writes get slower"]
STOP["kL0 StopWritesTrigger 1362<br/>writes stall until compaction drains"]
end
WR --> MUTEX
MUTEX --> BGT
BGT --> SLOW --> STOP
SN -.->|"bounds what compaction may drop"| BGT
Diagram source — enable JavaScript to render the diagram.4. API and extension design
- The public surface (
include/leveldb/) is a masterclass in small C++ APIs:DB::Put/Get/Delete/Write(WriteBatch),Iterator(with the Seek/Next/Valid contract),Slice(non-owning view — no string copies across the boundary),Status(state machine, not exceptions). - Seams, each one interface:
Comparator(custom key orders — the engine never assumes bytes),Env(include/leveldb/env.h— all OS dependency: files, time, threads;env_posix.cc/env_windows.ccare the two real implementations),FilterPolicy(pluggable Bloom or none),Cache(block cache as a sharded LRU, swappable), plus C bindings (db/c.cc) for embedding from other languages. - Honest wart: a single background thread means compaction and flush contend; no range deletes; no built-in checksums per-file beyond blocks. RocksDB exists to fix exactly these — read it next to see how requirements erode a clean design.
5. The defining trade-offs
Trade-off 1: LSM (log + merge) over B-tree (in-place update)
- Tension: disks/streaming media love sequential appends; point reads love sorted in-place pages.
- Choice: writes only append (WAL) or insert (memtable); sorted order is created later by merging immutable files.
- Evidence:
DBImpl::Write(1206) touches only log + skiplist; ordering emerges inBackgroundCompaction(708) →DoCompactionWorkmerging iterators (a k-way merge overMergingIterator). - Given up: read path touches memtable + imm + every level whose
range covers the key (
Version::Get, version_set.cc:324) — read amplification is the tax; mitigated by Bloom filters (util/bloom.cc:CreateFilter). - Verdict: right for write-heavy/scan-heavy workloads (queues, feeds, telemetry); wrong for strict p99 point-read stores — the exact inverse of a B-tree engine like SQLite's layer.
Trade-off 2: immutable files + manifest over mutable file map
- Tension: the level layout changes constantly; changing it safely under crashes needs its own transaction mechanism.
- Choice: files never change; layout changes are logged edits; live layout = replay of log.
- Evidence:
VersionSet::LogAndApply(777) writes the edit then installs the newVersion; deletes of obsolete files happen only after the edit is durable. - Given up: an extra level of indirection at startup (replay), and orphan-file GC.
- Verdict: this is the "WAL your own metadata" pattern — steal it for any system whose configuration must survive crashes.
Trade-off 3: leveled compaction with write-stall backpressure over proportional resources
- Tension: merge work must keep up with write rate; bounding space and read amp requires eventually compacting everything.
- Choice: level sizes grow ×10 (
MaxBytesForLevel, version_set.cc:41); L0 overlaps, deeper levels are disjoint sorted runs; compaction picks by score (size/level_bytes, line 1056); when L0 backs up, writes slow then stall (MakeRoomForWrite, 1331). - Evidence: the two triggers (
kL0_SlowdownWritesTrigger1342,kL0_StopWritesTrigger1362) — latency degradation as a designed control system input, not an accident. - Given up: bounded worst-case write latency (stalls are real); tuning is per-workload.
- Verdict: right when throughput is the product and slowdowns are acceptable; RocksDB's tiered compaction and rate limits are the industrial-strength evolution of this same dial.
6. Reading path
include/leveldb/db.h+include/leveldb/write_batch.h— the whole user contract in two headers (~200 lines): noteSlice,Status, and that Delete is Put with a tombstone.db/db_impl.cc:DBImpl::Write(1206) — the write spine end-to-end: writer queue, group commit, WAL append (mutex released!), memtable insert, sequence bump.db/log_format.h(whole file, ~35 lines) +db/log_writer.cc— the record/block framing; thendb/log_reader.cc:ReadRecordfor the crash-resync story.db/skiplist.h— one of the most-commented skip lists anywhere: read the template, the height probability (line 242), and the memory barriers discussion; thendb/memtable.cc(200 lines) to see the(key,seq)internal-key encoding (db/dbformat.h).db/db_impl.cc:MakeRoomForWrite(1331) — where flushes are triggered and where writes learn to wait; identify the two triggers.db/builder.cc:BuildTable(17) +table/table_builder.cc— watch a memtable iteration become an immutable file: blocks, restart points (prefix compression), index entries.table/format.cc(Footer, line 32) +table/table.cc— read path of one file: footer → index → two-level iterator → block; thentable/filter_block.cc+util/bloom.cc— the Bloom filter that makes level scans affordable.db/db_impl.cc:DBImpl::Get(1121) — the full read spine: snapshot seq, mem → imm → Version search, ref-counting to read lock-free.db/version_set.cc:Version::Get(324) — per-level file selection (SomeFileOverlapsRange, level search order); notice seeks count as compaction-triggering reads (seek compaction).db/db_impl.cc:BackgroundCompaction(708) andDoCompactionWork— the k-way merge, dropping shadowed versions belowsmallest_snapshot(lines 910-971), installing viaVersionSet::LogAndApply(777).db/version_set.cc:VersionSet::Recover(861) — startup as replay: CURRENT → manifest edits → Version; thendb/repair.cc— the disaster-recovery tool that rebuilds a manifest from orphan files.
You have now traced one key from Put to disk, through crash, restart,
compaction, and back out of Get — the complete life of an LSM record.
7. What to steal
- Make writes append-only and buy order later — sequential I/O plus background merge is the throughput recipe for any disk-bound system.
- Log your own metadata (manifest pattern): configuration changes get the same crash-safety machinery as data, instead of ad-hoc two-phase file swaps.
- Version your keys, snapshot by sequence number — MVCC falls out of carrying the version in the key; readers never block writers.
- Degrade before you fail: slowdown triggers then stop triggers (MakeRoomForWrite) turn overload into latency, protecting correctness and memory bounds.
- Quarantine the OS behind one class (
Env) — LevelDB ports to a new platform by implementing an interface, not by sprinkling ifdefs.
8. Difficulty and prerequisites
- Difficulty: moderate — the friendliest complete engine in the atlas; comments are plentiful and the C++ is deliberately plain.
- Budget: ~6 hours for the reading path; the whole
db/+table/core is readable in a focused week. - Prerequisites: skip lists (LevelDB's own comments suffice), what a
Bloom filter is (any 5-minute explainer), mmap basics for reading
table.cc.
9. Further reading
- The LevelDB README and
doc/directory in-repo (log format, impl notes). - O'Neil et al., "The Log-Structured Merge-Tree" (1996) — the paper this engine implements.
- RocksDB's wiki (https://github.com/facebook/rocksdb/wiki) — read as "what production demanded after this design."