Why architecture matters here

Every storage engine sits somewhere on the RUM triangle — Read, Update, Memory (space) — and you cannot optimize all three at once. A B-tree updates in place: one logical write is roughly one page write, so its write amplification is low, but random updates scatter across the disk and its reads pay for the tree's height. An LSM engine makes the opposite bet: it batches writes in memory and flushes them sequentially, achieving write throughput a B-tree cannot touch, but it accepts that the same key will be physically rewritten many times as background compaction reorganizes the data into a searchable shape. Write amplification is the price tag on that bet, and if you do not know the number you are operating blind.

The stakes are concrete and financial. SSDs are rated for a fixed endurance — a certain number of drive-writes-per-day over a warranty period, ultimately a total-bytes-written ceiling on the NAND. If your application writes 100 GB a day but a write amplification factor of 20 turns that into 2 TB of physical writes a day, you are consuming your endurance budget twenty times faster than the raw workload implies, and a drive rated for five years fails in three months. Amplification also steals bandwidth: compaction I/O and foreground I/O share the same device queue, so an engine spending 90% of its write bandwidth rewriting old data has only 10% left for the writes users actually issued — which shows up as latency spikes, write stalls, and the dreaded 'the database was fine and then it wasn't' incident.

The trap is that write amplification does not sit alone; it is one vertex of a triangle you are always moving around rather than escaping. Push it down with size-tiered compaction and you push read amplification up, because a query may now consult several overlapping sorted runs instead of one merged level. Push it down by keeping fewer, larger files and you push space amplification up, because obsolete versions linger longer before being compacted away. Every lever an LSM exposes — level multiplier, compaction strategy, memtable size, bloom-filter bits — is really a dial that trades one of these three costs for another, and the only way to choose well is to know which of read latency, write throughput, or storage cost your workload actually cares about most. A team that optimizes write amplification in isolation, without naming which other cost it is willing to pay, usually discovers the answer the hard way when read latency or disk usage becomes the new incident.

The reason this is an architecture problem and not a tuning afterthought is that the amplification factor is set by structural choices — the compaction strategy, the level size multiplier, the number of levels, the memtable size — made when the table is designed, and changing them later means rewriting the entire dataset. Teams that treat write amplification as a first-class design metric pick a compaction strategy matched to their read/write mix and size their levels for their hardware; teams that ignore it discover the number the hard way, during an outage, when compaction debt has already buried the cluster.

Advertisement

The architecture: every piece explained

Top row: the write path into the engine. A client write first appends to the write-ahead log — a sequential, fsync'd file that provides durability with amplification of almost exactly 1x (plus a little for log block alignment). The change is simultaneously inserted into the memtable, an in-memory sorted structure (a skip list, typically) that absorbs writes at memory speed and keeps them ordered by key. When the memtable fills, it is frozen and flushed to disk as an immutable, sorted SSTable at L0 — the first physical copy of the data in its long-term columnar-ish sorted-run form. So far amplification is modest: one WAL write plus one flush write, roughly 2x.

Middle row: where the real amplification lives — compaction. LSM engines organize SSTables into levels, each typically ten times larger than the one above it. L0 files can overlap in key range; to keep reads efficient, a compaction job reads overlapping files from L0 and the top of L1, merges them, discards overwritten and deleted keys, and writes fresh, non-overlapping SSTables back into L1. That single logical row now exists in an L1 file — its second full rewrite. As more data arrives, L1 fills and compacts into L2, rewriting the row again; L2 into L3; and so on. With a 10x level multiplier and leveled compaction, a key can be rewritten once per level it passes through — commonly giving an on-engine write amplification of 10–30x. This is the fundamental cost: keeping the data sorted for fast reads requires continuously re-merging it.

The engine offers levers that trade one amplification for another. Bloom filters and the block cache reduce read amplification so you can tolerate more sorted runs; tiered (size-tiered) compaction rewrites less often than leveled compaction — lower write amplification — but leaves more overlapping runs, raising read and space amplification (Cassandra's classic STCS-vs-LCS choice). A larger memtable means fewer, bigger flushes and less L0 churn. Every one of these knobs moves the workload around the RUM triangle rather than escaping it.

Bottom row: the second amplifier nobody controls from SQL — the SSD flash translation layer. NAND flash cannot overwrite a page; it must erase a whole block (hundreds of pages) first. The FTL writes new data to fresh pages and later runs garbage collection, copying still-valid pages out of a target block so the block can be erased — physical writes the host never issued. This device-level write amplification stacks multiplicatively on the engine's: a 15x engine factor on a drive doing 1.5x FTL amplification is 22.5x total. The ops strip names what actually keeps the number bounded — measuring the bytes-written-to-bytes-ingested ratio, throttling compaction so it never starves foreground I/O, sizing levels for the device, and budgeting SSD endurance explicitly.

Write amplification — one logical write, many physical writesthe tax LSM engines and SSDs charge on every byteClient write1 logical rowWAL appenddurability, 1xMemtablein-memory sortedFlush -> L0 SSTablefirst on-disk copyL0 -> L1 compactrewrite overlapL1 -> L2 compactrewrite againLn levels10x size ratioSSD FTLGC + page eraseRead/space tradebloom + block cacheMetricsbytes-written / bytes-in ratioOps — compaction throttle + level sizing + SSD endurance budgetflushcompacterase blocksamplifyamplifymeasureoperateoperate
Write amplification: a single logical write becomes a WAL append, a flush, and repeated compaction rewrites up the LSM levels, then SSD garbage collection multiplies it again.
Advertisement

End-to-end flow

Follow one row through a RocksDB-style leveled engine sized with a 128 MB memtable and a 10x multiplier. A service updates a user's last-seen timestamp: 64 bytes of key-value. The change appends to the WAL (64 bytes fsync'd, call it one physical write) and lands in the active memtable's skip list. The user is active, so this same key is updated forty more times over the next hour; each update is a fresh entry in the memtable and the WAL — the old versions are simply shadowed, not replaced, because LSM never mutates in place.

The memtable fills after absorbing a few million writes and is flushed to an L0 SSTable. During the flush the engine collapses the forty-one versions of our key into one (the memtable was already sorted and deduplicated on read), so only the latest timestamp is written to L0. This is the LSM's saving grace on hot keys: high-churn keys are cheap because compaction folds their history away. Our key now sits in an L0 file — physical rewrite number two after the flush.

L0 accumulates four files and triggers a compaction into L1. The engine reads the overlapping L0 files plus the relevant L1 SSTables, merges them, and writes new L1 files — our key is rewritten a third time, now bundled with thousands of neighbors in its key range. Weeks pass; L1 fills and compacts into L2 (rewrite four), and eventually into L3 (rewrite five) as the dataset grows to terabytes. A key that changes rarely after its initial burst still gets carried down through the levels by compaction of its neighbors — this is the space-driven component of amplification, independent of how often the key itself changes. Measured over the table's life, this cold key was written to flash five to six times for one logical lifetime of updates; a uniformly hot workload pushes the engine's aggregate factor into the 15–25x range.

Now the SSD's turn. Those compaction outputs are large sequential writes — the FTL's best case — so device amplification stays near 1.1x while free blocks are plentiful. But run the drive at 90% full for a year and free space fragments: the FTL must garbage-collect partially-valid blocks to make room, copying live pages and pushing device amplification toward 2–3x. The stall arrives on a quarter-end traffic spike: foreground writes surge, the memtable flushes faster, L0 fills faster than compaction can drain it, and the engine — to protect read latency from an explosion of overlapping L0 files — throttles and then stalls incoming writes until compaction catches up. The dashboard shows write latency jumping from 2 ms to 800 ms with no code change: the cluster hit its compaction debt ceiling, and the only real fix is more compaction bandwidth or less amplification, both of which had to be designed in earlier.