Write Amplification¶
What it is¶
Write amplification (WA) is the ratio of bytes physically written to storage to bytes the application logically asked to write:
write amplification = bytes written to the device / bytes written by the user
A WA of 10 means each 1 MB of user data costs 10 MB of disk writes. It matters for three reasons:
- Throughput: disk write bandwidth is finite. At 10x WA, a device that sustains 1 GB/s of writes supports only 100 MB/s of user writes.
- Device lifetime: SSD flash cells survive a limited number of erase cycles; drives are rated in total bytes written (TBW) or drive-writes-per-day. 10x WA wears the drive 10x faster. See Storage Hardware.
- Latency: background rewriting competes with foreground reads and writes for the same device.
It always comes paired with its siblings — read amplification (reads per lookup) and space amplification (disk bytes per live byte) — and reducing one usually raises another. See Storage Engines.
Where it comes from: every layer multiplies¶
The factors multiply: an engine at 10x running on an SSD with 2x internal amplification produces 20x at the flash.
B+Tree engines¶
| Source | Why |
|---|---|
| Page granularity | Changing one 100-byte row dirties a whole 8 KB page. If that page is written back before another change hits it, WA for that update ≈ 80x |
| Every index | One row insert modifies the heap page plus one page per index |
| WAL | Every change is written once to the log, then again to the page |
| Torn-page protection | PostgreSQL full-page images after each checkpoint; InnoDB doublewrite buffer — page content written twice |
| Splits | A split writes two pages plus the parent |
The saving grace is write combining: if a hot page receives 100 updates between checkpoints, it is written back once, so its per-update cost falls by 100x. B+Tree WA is therefore worst for random updates over a dataset much larger than memory — every update dirties a different cold page.
LSM engines¶
Each byte is written to the WAL, written again when the memtable is flushed, and then rewritten every time compaction moves it to a deeper level.
Worked example (leveled compaction):
- Size ratio between levels: 10.
- Levels: L0, L1 ... L4.
- Moving data from Ln to Ln+1 merges it with roughly 10x as much overlapping data in Ln+1, and rewrites all of it. Per byte entering a level, about 10 bytes of the next level are rewritten — though in practice it is often closer to half that, since overlaps are partial.
WAL 1
Flush to L0 1
L0 → L1 ~1–2
L1 → L2, L2 → L3, L3 → L4 ~5–10 each → ~15–30
---------------------------------------
Total ~18–35x
That matches the 10–30x commonly observed for RocksDB's default leveled compaction. Tiered compaction rewrites each byte only about once per tier, giving a much lower WA (often 2–10x) at the cost of more overlapping files to read and more temporary space.
Despite a higher ratio than a B+Tree might show for a hot working set, LSM writes are large and sequential, which is cheap for both HDDs (no seeks) and SSDs (whole erase blocks freed together, little FTL garbage collection). So "higher WA" does not automatically mean "slower."
Copy-on-write B+Trees¶
LMDB, BoltDB, and Btrfs never modify a page in place: updating a leaf
writes a new leaf, a new parent pointing at it, and so on up to a new
root. One small update writes height pages — ~3–4 pages, so tens of
KB for a 100-byte change — unless many updates are batched into one
transaction, which shares the rewritten upper levels.
The SSD layer¶
Inside the SSD, the FTL relocates still-valid pages before erasing a
block. Device-level WA depends on how full the drive is and how the
data dies: random small overwrites on a nearly full drive can push it
above 3–5x; large files written and deleted whole stay close to 1x.
TRIM (discard) and keeping free space on the drive keep it low.
Measuring it¶
# Bytes written by a process (Linux): write_bytes counts bytes sent to the block layer
cat /proc/$(pgrep -f db_bench)/io
# Device-level: watch wkB/s while the benchmark runs
iostat -x 1
# SSD lifetime counters (vendor-specific fields)
sudo smartctl -a /dev/nvme0n1 | grep -i "data units written"
Compare device bytes written against the bytes your benchmark
inserted. RocksDB also reports it directly in its compaction statistics
(the W-Amp column in rocksdb.stats).
Mitigation¶
| Technique | Applies to | Effect |
|---|---|---|
| Fewer indexes | B+Tree | Each removed index removes one page write per row change |
| Batch writes / larger transactions | All | More changes per page written; fewer WAL syncs |
| Larger memtable | LSM | Fewer, larger L0 files; more overwrites absorbed in memory |
| Tiered / universal compaction | LSM | Lower WA, higher read and space amplification |
| Key-value separation (WiscKey, BlobDB) | LSM | Large values written once to a value log; compaction moves only keys |
| Compression | All | Fewer physical bytes per logical byte |
| Free space + TRIM | SSD | Lower FTL garbage-collection cost |
Common pitfall¶
Adding indexes freely because "reads need to be fast," without weighing
the write side, is how write amplification quietly creeps up on a
write-heavy table — every additional index is another mandatory page
write on every insert, update, and delete, whether or not that index is
ever used by a query. See Index Types for when an
index's write cost outweighs its read benefit. PostgreSQL's
pg_stat_user_indexes.idx_scan = 0 finds indexes that are paying the
write cost without ever serving a read.