Skip to main content

Reference: Storage Engines — B-Tree, LSM-Tree, and Bitcask

A reference on single-node storage engines — B-tree, LSM-tree, and Bitcask — covering the write-ahead log, compaction, bloom filters, and how to choose.

What it is

A storage engine is the single-node component that turns get and put into disk operations: how a write becomes durable, how a key is found, and how space is reclaimed. Three designs cover almost every database you will name in an interview. A B-tree updates pages in place; an LSM-tree appends writes and merges them in the background; Bitcask appends to a log and keeps every key in an in-memory hash index.

Important

Key takeaway: The engine decides the read/write amplification trade-off, and the wrong choice caps your p99 no matter how well the distributed layer is designed. Append-dominated writes (events, metrics, versioned records) want an LSM-tree; read-heavy OLTP with range scans wants a B-tree; a key set that fits in RAM with point reads wants Bitcask.

When you care

Any “which database” answer eventually hits “and what is the storage engine underneath?”, and the follow-up “why is this write path fast?” is answered here. In most interviews, naming the engine and giving one sentence on why it matches the write pattern is enough. Go deeper (compaction strategy, bloom filter math) only when the interviewer pulls.

Note

Interview signal: “LSM, because every write is a fresh versioned record, so the write path is a sequential append and the cost moves to background compaction” is the one-sentence answer. Naming the read-side price (read amplification, mitigated by bloom filters) in the same breath is what makes it senior.

The three engines

EngineWrite modelRead modelGood forWatch out for
LSM-tree (RocksDB, Cassandra, LevelDB)Append to a log and an in-memory table; flush to immutable sorted files; merge in the backgroundA point read checks the memory table, then a bloom filter and index per fileWrite-heavy workloads, large datasets, compressible valuesRead amplification (several files per read); compaction I/O competing with foreground traffic
B-tree (InnoDB, PostgreSQL)Update the page in place; write-ahead log for durabilityO(log n) traversal; excellent range scansRead-heavy, range queries, OLTPWrite amplification from random in-place updates; page splits and fragmentation under heavy writes
Bitcask (Riak’s default)Append to a log; an in-memory hash maps every key to its disk offsetOne disk seek: hash lookup, then read at the offsetA key set that fits in RAM; the fastest point readsAll keys must fit in memory; no range queries; needs a merge to reclaim space

The write-ahead log

Both B-trees and LSM-trees put a write-ahead log (WAL) in front of everything. A write is first appended to the log (a sequential write, forced to disk), and only then applied to the in-memory structure. If the process crashes, the log is replayed to rebuild what was only in memory. The log is why a database can acknowledge a write without having updated its main data structure yet.

The durability knob is when the log is forced to disk: on every write (safest, slowest), in small groups (group commit, the usual compromise, many writes share one flush), or periodically (fast, with a window of possible loss). Say which you are choosing when you claim “durable”.

How an LSM-tree works

LSM-tree paths: a write is appended to the write-ahead log and inserted into the memtable; when the memtable is full it is flushed as an immutable sorted file; background compaction merges files and drops stale versions and tombstones; a read checks the memtable, then for each file a bloom filter and index, and merges versions across levels

Writes are sequential appends. Reads pay for it by looking in more than one place.

  1. Write. Append to the WAL; insert into the memtable (a sorted in-memory structure).
  2. Flush. When the memtable is full, write it out as an immutable sorted file (an SSTable) at the top level. Sequential, fast.
  3. Compaction. Background jobs merge files, discard overwritten versions and deleted keys (tombstones), and keep the number of files small.
  4. Read. Check the memtable; then, for each file, ask its bloom filter whether the key could be there, and only read the file’s index and block if the answer is “maybe”. Merge the versions found and return the newest.

Compaction is the maintenance cost, and it has two main strategies:

StrategyHowTrade-off
Leveled (RocksDB default)Each level has a size limit; files within a level cover disjoint key rangesBetter reads (a key lives in at most one file per level); higher write amplification
Size-tiered (Cassandra default)Merge files of similar size togetherBetter write throughput; worse reads (a key may be in several files)

The practical rule: leveled for read-heavy, size-tiered for write-heavy. An engine serving roughly three reads per write sits near the middle, so leveled with a generous compaction I/O budget is the usual call.

Worked example: why bloom filters pay for themselves. A point read for a key that does not exist would otherwise probe every file. With, say, 5 candidate files and a bloom filter at about 10 bits per key (roughly a 1% false-positive rate and no false negatives), the expected number of unnecessary file reads falls from 5 to 5 × 0.01 = 0.05. At a million reads a second, that is the difference between saturating the disks and barely touching them, which is why bloom filters are the LSM-tree’s standard read optimization. The cost is memory (about 10 bits per key per file) and a small false-positive rate.

Warning

Production reality: Compaction is where LSM engines get hurt. A burst of writes can build a backlog of files faster than compaction merges them; reads slow down (more files per read), and eventually the engine throttles writes to catch up. Watch the compaction backlog and the number of files per level, and give compaction its own I/O budget instead of letting it fight foreground traffic.

Choosing

WorkloadEngineBecause
Write-heavy ingest (time series, events, logs, metrics)LSM-treeSequential appends; cost moves to the background
Mixed with versioned or append-only recordsLSM-treeOverwrites and deletes are just new records; compaction collapses them
Read-heavy OLTP, range scans, predictable latencyB-treeOne traversal, no multi-file merge
Billions of small keys that fit in RAM, point reads onlyBitcaskOne seek per read, unbeatable throughput
Many secondary indexesBe careful with eitherEach index multiplies write amplification

Bitcask’s limit is arithmetic: an index entry of about 64 bytes per key means a billion keys need roughly 64 GB of RAM, so 100 billion keys need about 6.4 TB, which is why it fits only small key sets.

Caution

Common mistake: Treating an LSM-tree as “faster” without the read-side cost, or a B-tree as “slower” without the workload. The engines move cost between reads and writes; neither removes it. Choose by which side your workload is dominated by.