Skip to main content
databases

LSM trees

12 min read
Fully authored

Write-optimized storage — how LevelDB, RocksDB, and Cassandra store data.

In 1996, three researchers — Patrick O'Neil, Edward Cheng, Dieter Gawlick, and Elizabeth O'Neil — published a paper titled "The Log-Structured Merge-Tree." They were looking at the same problem as Bayer & McCreight had 26 years earlier, but from the opposite angle: instead of optimizing reads, what if you optimized writes?

Their insight was elegant: never update disk data in place. All writes go to an in-memory buffer (the MemTable). When it fills up, dump it to disk as a sorted, immutable file (an SSTable — Sorted String Table). Never touch that file again — just create new ones as more data comes in. Reads have to check multiple files, but every write is a fast sequential append.

For a decade the paper was ignored. Then in 2006, Google published the BigTable paper. Then Amazon published Dynamo (2007), Facebook released Cassandra (2008), Google open-sourced LevelDB (2011), and Facebook released RocksDB (2013). Every one uses LSM-trees. Today, LSM-trees are the storage engine inside Cassandra, RocksDB, LevelDB, HBase, ScyllaDB, TiKV, InfluxDB, ClickHouse (partially), and the write layers of CockroachDB, TiDB, YugabyteDB, and MongoDB WiredTiger's LSM mode.

The papers

  • O'Neil, Cheng, Gawlick, O'Neil (1996) — "The Log-Structured Merge-Tree." Acta Informatica. The original.
  • Chang et al. (2006) — Google Bigtable. First industrial LSM at massive scale.
  • Lakshman & Malik (2010) — Cassandra. "Dynamo + Bigtable": LSM for the storage engine.
  • Dong et al. (2017) — "Optimizing Space Amplification in RocksDB." Facebook engineers on how to tune LSM compaction.

The core idea — never write in place

A B-tree write updates a page in place: read the page into memory, modify it, write it back to the same disk location. This random write is the expensive part. LSM-trees never do it. Every write is a sequential append — first to the WAL (for durability), then into the in-memory MemTable. When the MemTable fills (typical 64 MB), it's flushed to disk as an SSTable — another sequential write. SSTables are immutable once written.

LSM write path — step 1 of 6
Client → WAL → MemTable → (eventually) SSTable.
ClientWAL (disk)MemTable(RAM, sorted)Flush → new SSTSSTable 1SSTable 2MemTable size: 45 MB / 64 MB
Client sends PUT('user123', {...}). Application-layer write.

Sequential writes are ~100× faster than random writes on both SSDs (avoiding write amplification) and traditional HDDs (avoiding seek time). LSM-trees exploit this ruthlessly. A Cassandra node can ingest hundreds of thousands of writes per second per node because every write is a sequential append.

The three-part architecture — MemTable + WAL + SSTables

LSM three-part architecture
MemTable (RAM)
Sorted in-memory skip list. All writes go here first (after WAL).
Size: ~64 MB typical
Structure: skip list, red-black tree
Purge: flushed to SSTable when full
WAL (disk)
Sequential log of every write. Only for durability — reads never touch it.
Format: append-only file
fsync: after N writes or M ms
Purge: truncated after MemTable flush
SSTables (disk)
Immutable, sorted, on-disk files. Each has an index + bloom filter.
Naming: monotonically-increasing IDs
Reads: bloom filter → block → binary search
Purge: compaction merges + deletes them

MemTable is a sorted in-memory data structure — usually a skip list. Reads check it first. Writes go into it. When it fills, it's flushed to disk. WAL (Write-Ahead Log) is a sequential log of every write, appended before the write hits the MemTable. On crash, the WAL replays to reconstruct the MemTable. SSTables are the immutable on-disk files — sorted key-value pairs with an index at the end. Once written, never modified. Only deleted (during compaction).

Compaction — the maintenance ritual

If you never rewrite, you accumulate SSTables forever. Reads would have to check all of them (bad), and space would grow (worse — updates and deletes leave "shadow" entries). The solution: compaction. Periodically, the engine merges multiple SSTables into one — dropping deleted keys and keeping only the latest value for each key.

LSM compaction — step 1 of 3
Merge multiple SSTables into one, dropping duplicates.
Level 0 (before)
SST-1
a=1, b=2, c=3
SST-2
a=4, d=5, e=6
SST-3
b=7, c=8, f=9
Level 1 (after)
(compacting…)
Three SSTables at L0. Notice: 'a' exists in SST1 (a=1) AND SST2 (a=4). SST2 is newer, so a=4 wins.

Compaction is the single biggest tuning knob in LSM systems. More aggressive compaction = fewer SSTables to check = faster reads, but higher write amplification and disk I/O. Less aggressive = faster writes but slower reads. RocksDB has tiered, leveled, and hybrid compaction strategies — each a different point in the trade-off. Cassandra's SizeTiered vs LeveledCompactionStrategy is the same choice.

Reads — the hard part

Reads must check every place the key could be: MemTable first (newest), then SSTables from newest to oldest. Naïve implementation would read every SSTable on every miss. Two techniques make it fast:

  • Bloom filters — each SSTable has a bloom filter for its keys. Before reading the SSTable, check the bloom filter. If it says "definitely not here," skip the SSTable entirely. Typical: 1% false-positive rate at 10 bits per key.
  • Sparse indexes — each SSTable's footer has an index of one key per block (usually 4 KB). Binary-search the index to find the right block, then linear-scan.
Bloom filter — the read-path accelerator
Definitely NOT here
Skip this SSTable entirely. No disk I/O.
Maybe here (1% false-positive)
Have to check. 99% of the time the key IS actually there.
10 bits per key
Standard configuration. 1M keys = 1.25 MB filter in RAM.
The math: A bloom filter is a bit array + multiple hash functions. Set bits when inserting a key. Check if all bits are set when querying. If any bit is 0, key is definitely absent. If all bits are 1, key is probably there (or a false positive).

With bloom filters and sparse indexes, a well-tuned LSM typically needs 1-2 SSTable reads on average per point lookup. Range scans are more expensive — they must merge across all SSTables — which is why Cassandra range queries are often constrained to a single partition.

Compaction strategies — tiered vs leveled

Tiered (SizeTiered in Cassandra)
How: When N SSTables of similar size accumulate, merge them into one bigger SSTable. Repeat at every level.
✓ Pros: Fast writes; low write amplification.
✗ Cons: More SSTables → slower reads; large temp space needed for merge.
Cassandra default, LevelDB L0-only
Leveled (LeveledCompactionStrategy)
How: Each level Ln is 10× bigger than L(n-1). Non-overlapping SSTables within a level. Each key exists once per level.
✓ Pros: Predictable read cost (1 SSTable per level).
✗ Cons: Higher write amplification (~30x); more compaction work.
RocksDB default, Cassandra LCS, LevelDB L1+
Hybrid (Universal in RocksDB)
How: Mostly tiered, but bounded — never let L0 grow past a threshold.
✓ Pros: Balances writes and reads.
✗ Cons: Complex to tune.
RocksDB Universal, some Cassandra deployments
Time-window (TWCS)
How: Group SSTables by write time. Only compact within a time window. Never compact across windows.
✓ Pros: Perfect for time-series (data expires by TTL).
✗ Cons: Not general purpose.
Cassandra TimeWindowCompactionStrategy (metrics, IoT)

The write-amplification trade-off

Every LSM has a cost: write amplification. A single byte written by the application ends up being written to disk multiple times as it moves through compaction levels. Typical: 10-30x write amplification. This is why LSM-based databases wear out SSDs faster than B-tree databases. RocksDB engineers have spent enormous effort minimizing this (see Facebook papers on tuning).

The trade-off table:

  • LSM: high write throughput, low read latency on point lookups (with bloom filters), high write amplification, disk-hungry compaction.
  • B+tree: high read throughput, fast range scans, low write amplification, but writes are slower under concurrency.

Applied in real systems

RocksDB
Deep dive

RocksDB — Facebook's embedded LSM

Facebook forked LevelDB in 2012 and made it multi-threaded, highly-tunable, and production-hardened. Backs LinkedIn (Espresso), TiKV, CockroachDB, YugabyteDB, Kafka Streams, Flink, Rockset. The most influential embedded LSM in the world.

Read the deep dive →
LevelDB
Deep dive

LevelDB — Google's original open-source LSM

Google open-sourced LevelDB in 2011 by Jeff Dean + Sanjay Ghemawat. Simpler than RocksDB, single-threaded compaction. Used by Chrome (IndexedDB backend), Bitcoin Core wallet, Ethereum clients.

Read the deep dive →
Apache Cassandra
Deep dive

Cassandra — Dynamo + Bigtable = LSM at planet scale

Every Cassandra table is stored as MemTables + WAL + SSTables per node. Netflix runs 2500+ node Cassandra clusters; Discord ran trillions of messages this way. SizeTiered (default) or LeveledCompactionStrategy per table.

Read the deep dive →
ScyllaDB
Deep dive

ScyllaDB — Cassandra rewritten in C++

KVL of Israel. 2016. Same LSM architecture as Cassandra but in C++ + Seastar shard-per-core framework. 10× lower tail latency. Discord migrated from Cassandra to ScyllaDB in 2022.

Read the deep dive →
Apache HBase
Deep dive

HBase — the Bigtable open-source clone

2008 open-source Bigtable. Wide-column store on top of HDFS. Same LSM concepts. Powered Facebook Messages until they moved to MyRocks, and many Hadoop-era pipelines. Still runs at Yahoo, Xiaomi, Airbnb metadata.

Read the deep dive →
TiKV
Deep dive

TiKV — RocksDB for TiDB and CNCF

PingCAP's distributed KV, storage layer of TiDB. Wraps RocksDB with Raft replication. Every range is a Raft group; the local storage under each is RocksDB. Powers TiDB, TiCDC, and many CNCF projects.

Read the deep dive →
CockroachDB / Pebble
Deep dive

CockroachDB — from RocksDB to Pebble

CockroachDB started on RocksDB but wrote their own Go implementation (Pebble) in 2020 to eliminate the Go/C FFI cost and control GC. Pebble is now the default. Same LSM concepts, purpose-built for Cockroach's needs.

Read the deep dive →
InfluxDB IOx
Deep dive

InfluxDB — LSM for time-series

InfluxDB v3 (IOx, 2022+) uses an LSM-derived architecture in Rust based on Apache Arrow. Time-series writes are extremely append-heavy — LSM is the natural fit. Also used by ClickHouse for MergeTree engine (which is LSM-inspired).

Read the deep dive →

Key takeaways

  • LSM = MemTable + WAL + SSTables + compaction. Writes append; SSTables are immutable; compaction merges them.
  • Writes are sequential — 100× faster than random writes on both HDD and SSD. This is the LSM win.
  • Reads check multiple SSTables. Bloom filters skip irrelevant ones (~1% false-positive rate). Sparse indexes find the right block within an SSTable.
  • Compaction merges SSTables, drops deletes, and reclaims space. Tiered = faster writes; leveled = faster reads. Every LSM database has this tuning knob.
  • Write amplification is the cost — typically 10-30×. LSMs wear out SSDs faster than B-trees.
  • RocksDB, LevelDB, Cassandra, ScyllaDB, HBase, TiKV, Pebble, InfluxDB IOx — every write-heavy modern storage engine uses LSM. Learn one and you can read the source of the others.
  • The BigTable/Dynamo/Cassandra papers together made LSM the default write-heavy storage engine for the modern distributed data world.

References

  • O'Neil et al. (1996) — The Log-Structured Merge-Tree. Acta Informatica.
  • Chang et al. (2006) — Bigtable. OSDI.
  • Lakshman & Malik (2010) — Cassandra.
  • Dong et al. (2017) — Optimizing Space Amplification in RocksDB. Facebook.
  • Kleppmann (2017) Designing Data-Intensive Applications, Chapter 3.
  • Petrov (2019) Database Internals, Chapter 3 has the modern treatment.

Practice what you just read

Every foundation concept has a companion quiz to close the loop.