SSDs and the Flash Translation Layer

A solid-state drive presents the same face to the block layer as a hard disk — "here are N logical 4 KiB blocks, read and write them by number." That interface is a lie, and a beautiful one. Underneath, NAND flash obeys physics utterly unlike a rewritable disk: you can read a page, you can write (program) a fresh page, but you cannot overwrite a page in place. To reuse a page you must first erase the whole large block it lives in — hundreds of pages at once — and each cell survives only a few thousand of those erase cycles before it wears out.

Bridging that gap between the clean block interface and the messy flash reality is the job of the Flash Translation Layer (FTL) — a tiny log-structured operating system running on the drive's own controller. Understanding the FTL explains everything on an SSD datasheet: write amplification, over-provisioning, endurance ratings, TRIM, and why your drive sometimes stutters. If you have met log-structured file systems, you already know the tune; the FTL plays it in silicon.

The three flash operations and their brutal asymmetry

NAND flash has three primitives, and their costs and granularities differ by orders of magnitude — this asymmetry is the reason the FTL exists.

OperationGranularityRough latency
Reada page (4–16 KiB)~25–100 µs
Program (write)a page — but only if erased~200–900 µs
Erasea whole block (~128–256 pages)~2–5 ms

The killer constraints: you program at page granularity but erase at block granularity, and program can only turn 1→0, so a page must be erased (all 1s) before it can be written. You cannot flip one byte of a stored page; to change it you must write the new version somewhere else and mark the old one stale. Sound familiar? It is exactly the append-only log discipline — forced on you by the hardware.

The FTL: a log with a map

The FTL keeps a mapping table from logical block address (what the OS names) to the physical page currently holding that data. On a write, it does not touch the old page. It:

Because writes wander, the drive slowly fills with stale pages. Garbage collection reclaims them: pick a victim erase-block, copy its still-valid pages out to the log, then erase the whole block back to free. That copying is pure overhead — I/O the host never asked for. Meanwhile, wear leveling ensures the controller spreads erases across all blocks (not just the frequently-rewritten ones), so no single block wears out years before the rest, and TRIM lets the file system tell the drive "this logical block is now unused," letting GC drop that data instead of dutifully copying dead bytes forward.

Write amplification — the number that governs your SSD's life

Every valid page GC copies forward is a flash write the host never requested. So the flash does more writing than the host asked for. The ratio is write amplification:

\text{WAF} \;=\; \frac{\text{bytes actually written to NAND}}{\text{bytes written by the host}} \;\ge\; 1

A WAF of 3 means every 1 GB you write costs the flash 3 GB of wear. Since endurance is a fixed budget of program/erase cycles, WAF divides your drive's lifetime. And WAF depends sharply on how full the drive is: when GC victim blocks are mostly valid, it must copy a lot to reclaim a little. This is why drives ship with over-provisioning — hidden spare capacity (say 7–28%) the host cannot see — that keeps a healthy pool of free blocks so GC always has slack and WAF stays low.

// Write amplification from garbage collection. // A drive of `blocks` erase-blocks, each `pagesPerBlock` pages. To reclaim a victim block that is // `validFrac` valid, GC must copy that many valid pages elsewhere before erasing. const pagesPerBlock = 128; function wafForFullness(validFrac: number): number { // Reclaiming one block frees pagesPerBlock pages but only (1 - validFrac) of them are "new" space. // Host-useful writes per GC cycle = free pages produced; NAND writes = those + copied valid pages. const copied = validFrac * pagesPerBlock; // GC copy-forward writes const freed = (1 - validFrac) * pagesPerBlock; // space made available for host data if (freed === 0) return Infinity; return (freed + copied) / freed; // total NAND writes / host writes } console.log("victim valid % -> write amplification (WAF)"); for (const vf of [0.2, 0.5, 0.7, 0.9]) { const waf = wafForFullness(vf); console.log(` ${(vf * 100).toFixed(0)}% valid -> WAF ≈ ${waf.toFixed(2)}`); } console.log("\nFuller victim blocks (more valid pages) => higher WAF => faster wear."); console.log("Over-provisioning keeps victims emptier, holding WAF down.");

Endurance is quoted as P/E cycles (program/erase per block) or as TBW (terabytes written). A modern TLC cell tolerates roughly 1 000–3 000 P/E cycles (QLC fewer, ~1 000; old SLC ~100 000). Take a 1 TB TLC drive rated 600 TBW. Even writing a hefty 50 GB every single day, that is 600{,}000 / 50 \approx 12{,}000 days — about 33 years. For almost all users the drive is obsolete long before its flash wears out. The workloads that do kill SSDs are write-amplifying ones — tiny random overwrites on a nearly full drive with poor over-provisioning — which is exactly why datacentres run enterprise drives with 28% OP and care intensely about keeping WAF near 1.

A classic surprise: when you delete a file, the file system merely marks its blocks free in its own metadata. The SSD's FTL has no idea — as far as the drive knows, those logical blocks still hold valid data, so GC keeps dutifully copying them forward, inflating write amplification for no reason. The fix is TRIM (the discard command): the file system explicitly tells the drive "these logical blocks are dead, you may forget them." Only then can the FTL mark those pages stale and let GC drop them. Without TRIM an SSD slowly degrades as its internal view of "valid" data drifts far above the truth — which is why fstrim and the discard mount option exist, and why the FTL cannot simply infer freeing on its own.

The picture, stated as laws