HotShard
Open the simulatorSimulator β†’
Storage engine

LSM Tree

How write-heavy databases turn slow random writes into fast sequential ones.

You're taking 10,000 writes a second. Each one lands at a random spot in a huge file on disk. Random writes are the slowest thing a disk does, so this crawls. How do you keep up without hunting for the right spot every time? Answer that and you've rebuilt the LSM tree, the engine inside RocksDB, Cassandra, and LevelDB.

Open the simulator β†’~7 min read

Watch the whole topic Β· or read it below

The problem: writes land at random spots on disk#

TL;DRthe 30-second version
  • Don't update data where it lives on disk. That's a slow random write. Buffer writes in a sorted table in memory (the memtable), then write the whole batch to disk in one sequential sweep.
  • Log each write first (the WAL) so a crash can't lose it. When the memtable fills, write it out as an immutable sorted file (an SSTable).
  • A read checks newest to oldest. A Bloom filter lets it skip files that can't hold the key. A background merge (compaction) keeps the file count and stale data in check.
  • The bargain: very fast writes, paid for by rewriting data as it merges downward. This is the engine inside RocksDB, Cassandra, LevelDB, and ScyllaDB.

A normal database index updates a record in place. To write it, the engine finds the exact disk page the record belongs on and rewrites that page. A stream of writes to different keys becomes a stream of random disk jumps. Random writes are the slowest thing a disk does. A typical SSD sustains maybe 50–100 MB/s of small random writes, but 500+ MB/s writing sequentially. That's a 5–10Γ— gap.

So here's the whole idea. Stop hunting for the right spot. Never update data where it lives. Buffer writes in memory, and once you've gathered a batch, write them all to disk in one sequential sweep. A storm of random writes becomes a few big sequential ones. Now let's build it, one piece at a time.

The write path: WAL, memtable, SSTable#

Every write first lands in the memtable. That's a sorted structure in memory, usually a skip list or a red-black tree. Writes arrive at memory speed. Because the memtable keeps its entries sorted by key, the eventual write to disk needs no extra sorting.

The memtable lives in RAM, and RAM vanishes on a crash. So before a write enters the memtable, it's appended to the write-ahead log (WAL), a plain sequential file on disk. Appending to a file is fast. After a crash, the engine replays the WAL to rebuild whatever was in the memtable. Nothing is lost.

PUT user:1=alicea write comes in
append firstso a crash can't lose it
WALappended to a log on disk
then insertthe write is now readable
memtablekept sorted, in memory
Every write lands in memory, safely

When the memtable reaches its size limit, typically 64 to 256 MB, it's frozen and a new empty memtable takes its place. The frozen one is written to disk in one sweep as an SSTable, a Sorted String Table. Once written, an SSTable is never modified. That immutability is what keeps random writes out of the system. When the flush is done, the matching WAL segment is deleted, because the SSTable is now the durable copy.

An SSTable has a little structure inside so lookups are fast:

  • Data blocks: key-value pairs grouped into fixed-size blocks, typically 4–64 KB, sorted within and across blocks.
  • Block index: a small table at the end of the file with the first key of each block and its byte offset. A lookup binary-searches it and jumps straight to the one block it needs.
  • Bloom filter: a few kilobytes of bits that answer 'is this key definitely not in this file?' with no false negatives. If it says no, the whole file is skipped without reading a byte.

The read path: newest to oldest#

To look up a key, the engine checks each place from newest to oldest and stops at the first match. Newest wins, because the latest write is the current value.

Memtablein memory Β· newest writes
not herea Bloom filter saying 'definitely not here' skips the whole file
L0 SSTablesnewest first Β· ranges overlap Β· bloom-checked
not here
L1non-overlapping Β· at most one file Β· bloom + block index
not here
L2 … deepersame, until found or all levels exhausted
GET key: probe newest β†’ oldest, stop at the first match

L0 holds the newest flushed files, and their key ranges can overlap, so every L0 file gets checked. From L1 down, compaction (next) keeps each level's files non-overlapping, so a lookup touches at most one file per level.

PredictYou GET a key that was never written. How many SSTable data blocks does a well-tuned LSM actually read from disk?

Hint: What is the Bloom filter for?

Ideally zero. The Bloom filter on each SSTable answers 'definitely not here' for a key that was never added, so the engine skips every file without reading a data block. The cost is just the in-memory Bloom checks, plus rare false positives. A missing-key read is the case Bloom filters were invented to make cheap.

Deletes are writes: tombstones#

An SSTable is immutable, so you can't erase a key from one. Instead a DELETE writes a tombstone: a record with the key and a 'deleted' marker. It flows through the WAL and the memtable like any other write. On a read, the tombstone is found first and shadows the older value, so the key looks gone.

The old value is still on disk in an older SSTable. It's removed later, during compaction, when the tombstone and the value it shadows are merged together and both are dropped.

Compaction: merging the files back together#

Every flush adds an SSTable. More files means more files to check on every read, and more space wasted on stale versions and tombstones. Compaction is the background job that reverses this. It picks a set of SSTables, merges them keeping only the newest value per key, drops tombstones that have nothing left to shadow, and writes a smaller set of new files. The inputs are then deleted.

  • Leveled compaction: each level has a size cap, typically 10Γ— the level above. When L0 overflows, its files are merged into the L1 files whose key ranges overlap, and so on down. From L1 on, files within a level don't overlap, so a read touches at most one file per level. Reads and space stay cheap, but each byte is rewritten about once per level on the way down. Used by RocksDB's default mode, LevelDB, and Cassandra's LCS.
  • Size-tiered compaction: files of similar size are grouped into tiers. When a tier has enough files, typically 4, they're merged into one larger file. Each byte is rewritten far fewer times, so writes are cheaper. But a tier can hold many overlapping files, so reads probe more files, and a big merge can briefly need about 2Γ— the dataset's disk space. This is Cassandra's STCS, and the usual pick for ingest-heavy time-series.

Newest at the top Β· each level about 10Γ— larger

L0sstsstsstnewest β€” key ranges can overlap
L1sorted sstsorted sstnon-overlapping, sorted
L2sstsstsstsstabout 10Γ— larger again
The level structure compaction builds on disk

The three amplifications: write, read, space#

Every storage engine pays three costs. Lower one and you raise the others. An LSM tree makes a clear choice: cheap writes, paid for with some read and space amplification, both of which compaction keeps bounded.

PredictWith leveled compaction, each level is about 10Γ— the one above and there are ~7 levels. Roughly how many times does one byte get rewritten by the time it settles at the bottom?

Hint: It's rewritten about once per level as it merges downward.

Very roughly 10–30Γ— in practice (about the level multiplier Γ— the number of levels). So one byte you write can become 10–30 bytes actually written to disk. That's write amplification, the price an LSM pays to turn random writes into sequential ones and keep reads cheap.

  • Write amplification: bytes written to disk divided by bytes you logically wrote. Compaction rewrites data as it descends, so one ingested byte may be written 10–30Γ— before it settles at the bottom.
  • Read amplification: how many files one lookup touches. Without Bloom filters you'd read every SSTable. With them and leveled compaction, most lookups touch one or two files.
  • Space amplification: disk used beyond the minimum the data needs. Pending tombstones, stale overwritten versions, and overlapping files all add to it until compaction catches up.
When to reach for an LSM (and when not to)

Reach for an LSM when writes dominate or arrive in bursts you can't afford to block on, and you can accept slightly slower, less predictable reads. It's a poor fit when the workload is read-mostly with strict point-read latency targets, or leans on large range scans and in-place updates.

  • Good fit: write-heavy ingestion (metrics, logs, events, IoT), key-value and wide-column stores, time-series, and anything where ingest throughput is the bottleneck.
  • Weaker fit: read-mostly OLTP needing single-digit-millisecond point reads with tight tail latency, or heavy scan-and-update patterns. A B-tree's in-place, single-path layout wins there.
  • Watch out: delete- or update-heavy workloads create tombstones and stale versions that compaction must keep up with, or read latency and disk usage creep up.
The tuning knobsMemtable size: larger means fewer, bigger flushes (less write amplification) but more RAM and longer crash recovery. Bloom bits per key: more bits mean fewer false positives and less read amplification, at a small memory cost (~10 bits/key β‰ˆ 1% false positives). Compaction strategy: leveled for read-heavy, tiered for write-heavy. Block size: larger blocks compress better and speed scans; smaller blocks make point reads cheaper.
LSM tree vs B-tree
LSM treeB-tree
Write performanceExcellent β€” all writes are sequential appendsGood for light load; degrades under high write volume
Read performanceGood β€” bloom-filtered, but may probe multiple filesExcellent β€” predictable single-path lookup
Range scansReasonable β€” must merge across sorted SSTable filesExcellent β€” leaf linked list enables O(k) scan
Space usageTemporary bloat until compaction catches upCompact β€” in-place updates, no extra copies
Best forWrite-heavy: event logs, time-series, Cassandra, RocksDBRead-heavy OLTP, range queries: PostgreSQL, MySQL, SQLite
In the wild
  • LevelDB (Google): the compact reference implementation. It introduced the leveled-compaction design most others build on. Embedded, single-process.
  • RocksDB (Meta): a heavily extended fork of LevelDB. Multi-threaded compaction, column families, leveled/universal/FIFO compaction, tunable Bloom filters. The storage engine inside MySQL (MyRocks), CockroachDB, TiKV, and Kafka Streams.
  • Apache Cassandra / ScyllaDB: wide-column stores with SSTables per table and pluggable compaction (STCS, LCS, TWCS). ScyllaDB reimplements Cassandra in C++ on a shard-per-core architecture.
  • HBase / Bigtable: the original Bigtable paper's design. Memtable plus WAL plus SSTables on a distributed filesystem (HDFS or Colossus).

The same shape shows up outside databases too. Pebble (CockroachDB's Go LSM) and BadgerDB are embedded stores, and Lucene's index segments are immutable, merge-compacted SSTables for an inverted index.

Gotchas
  • Tombstone storms. Until compaction clears them, tombstones take space and must be checked on every read. Cassandra users hit this when deletes outpace compaction: reads slow down and disk usage bloats.
  • Over-aggressive compaction. It lowers read and space amplification but raises write amplification: more rewrites, more disk wear, more CPU. Run too hard, it starves foreground writes.
  • Treating the WAL as optional. The memtable is in RAM and vanishes on a crash. The WAL is the only durable copy of recent writes until the flush lands.

QuizA workload does millions of overwrites to a small set of hot keys. Which amplification suffers most, and which compaction strategy helps?

  1. Read amplification; switch to size-tiered compaction
  2. Space amplification; leveled compaction keeps stale versions bounded
  3. Write amplification; disable compaction entirely
  4. None; overwrites are free in an LSM
Show answer

Space amplification; leveled compaction keeps stale versions bounded β€” Repeated overwrites leave many stale versions of the same keys scattered across files. That's space amplification. Leveled compaction keeps each level non-overlapping and merges often, so old versions are collapsed quickly and space stays bounded, at the cost of higher write amplification. Disabling compaction would let stale data and files grow without limit.

In an interview

Lead with the trade. An LSM turns random writes into sequential ones by buffering in the memtable and flushing to immutable SSTables. That buys write throughput. The price is read amplification (several files to probe) and write amplification (compaction rewrites data as it moves down). Bloom filters and leveled compaction keep both bounded.

Then name the parts in order: WAL, memtable, SSTable (block index plus Bloom filter), compaction. Know leveled versus tiered. Know tombstones. Name RocksDB, Cassandra, LevelDB, and ScyllaDB.

Are LSM reads always slower than a B-tree's?

No. Hot keys live in the memtable or L0 and are very fast, and Bloom filters make missing-key reads nearly free. The weakness is the worst case: a key in a deep level, or a range scan that merges across many SSTables. It's the tail latency that suffers, not the average read.

Does a delete free disk space right away?

No. A delete writes a tombstone, an extra record, so disk usage briefly goes up. Space is reclaimed later, when compaction merges the tombstone with the value it shadows and drops both.

Is the WAL redundant once the write is in the memtable?

No. The memtable is in RAM and vanishes on a crash. The WAL is the only durable copy of recent writes until the memtable is flushed to an SSTable. Then that WAL segment can be dropped.

Leveled or tiered compaction: how do you pick?

Leveled for read-heavy work: a read touches at most one file per level and space stays tight, but data is rewritten about once per level, 10–30Γ— in total. Tiered for write-heavy ingest like time-series: far fewer rewrites, but reads probe more files and a big merge can briefly need 2Γ— the disk.

References & further reading
References

Feedback on this topic β†’