End-to-end checksums, scrubbing and bit-rot detection
What must a checksum cover to catch real corruption, and how often must you scrub before the last good copy expires?
One word, three failure models
The word "checksum" is used for three different jobs. They answer three different questions against three different adversaries:
| Question | Adversary | Mechanism |
|---|---|---|
| Are these the bytes I wrote? | an indifferent storage system | block checksums, scrubbing, parent-pointer hashes (this mechanism) |
| Are these the right values? | a buggy pipeline or upstream system | reconciliation, hash aggregates, cell-level diffs |
| Were these bytes written by someone authorized? | an intelligent attacker | MACs (message authentication codes), signatures, key management |
The checksum here must survive indifference, not malice. That is why CRC32C (a fast, simple checksum) is adequate here and completely inadequate against an attacker. Different threat model, different tool, same word.
The failure model, corrected
Folklore says "bit rot": charge slowly leaking from the medium. Field evidence says otherwise. Large studies (CERN's 2007 silent-corruption audit; Bairavasundaram et al.'s multi-year study of about 1.5 million drives) found that the corruption you actually meet is dominated by the path, not the medium:
- firmware bugs;
- misdirected writes: the drive wrote your block perfectly, to the wrong address;
- lost writes: acknowledged, never performed;
- torn writes;
- bad cables and host bus adapters (HBAs);
- memory without error correction (non-ECC DRAM);
- ordinary software bugs.
Rates are of the order of 10⁻⁴ to 10⁻² of drives per year showing at least one silent error. And, critically, errors are strongly correlated within one drive and within one manufacturing batch. That is exactly where redundancy plans that assume independence fail (Durability across machines).
This correction drives the whole design. Media decay would be caught by the drive's own error-correcting code (ECC). Misdirected and lost writes are not caught: the block that comes back is perfectly intact. It is just the wrong block, or the correct-looking stale one. No amount of integrity checking inside one device catches that.
The end-to-end argument, plus an anchor
The checksum must be computed by the party that produced the data and verified by the party that consumes it. And, the part that most designs get wrong, it must cover the block's identity, not only its contents. A checksum stored inside the block validates a misdirected write perfectly, because the block and its checksum travelled together and agree with each other.
Three real designs:
- ZFS stores each block's checksum in the parent block pointer, not in the block itself. A misdirected write cannot bring a matching checksum with it. The tree validates itself from the root.
- InnoDB and Postgres embed the page number in the page and checksum it. That detects misdirection within the file.
- T10 DIF/PI (a standard for data-integrity fields) attaches 8 bytes to each sector: a guard (checksum), a reference tag holding the expected block address (LBA), and an application tag. It is verified at every hop, from the host adapter to the media.
The general rule:
Six forms of the same rule: ZFS parent-pointer checksums, page number inside a page, T10 reference tags, Merkle trees (hash trees where each parent hashes its children), content-addressed stores (data named by its own hash), and table-format manifests (the data file is valid, but only the manifest says it belongs to the table). A seventh appears in distributed systems: fencing tokens, because a leader cannot prove from inside itself that it is still the leader.
Scrubbing, derived from a timing inequality
Latent corruption is found only by reading. Nobody reads cold data. So corruption collects exactly in the data least likely to be read, which is also the data whose redundant copies are aging in parallel, under the same conditions. Hence scrubbing: proactively reading all data and checking its checksums on a schedule.
The design constraint is an inequality that almost nobody writes down:
scrub_period + detection_to_alert_lag < retention_period of the last good copyCost scales with stored bytes, permanently:
- 100 TB on a 30-day cycle: 100 × 10¹² B ÷ (30 × 86,400 s) ≈ 40 MB/s of background reads. Practically free.
- 10 PB on the same cycle: ≈ 4 GB/s. Real capacity.
- time to read every stored block once
- how long backups keep a good copy
Data table
| Data stored | Scrub read rate (bytes/s) |
|---|---|
| 931 GB | 377 KB |
| 1.05 TB | 435 KB |
| 1.21 TB | 502 KB |
| 1.4 TB | 580 KB |
| 1.62 TB | 670 KB |
| 1.87 TB | 774 KB |
| 2.16 TB | 893 KB |
| 2.49 TB | 1.01 MB |
| 2.88 TB | 1.16 MB |
| 3.32 TB | 1.34 MB |
| 3.84 TB | 1.55 MB |
| 4.43 TB | 1.79 MB |
| 5.11 TB | 2.07 MB |
| 5.91 TB | 2.39 MB |
| 6.82 TB | 2.76 MB |
| 7.88 TB | 3.19 MB |
| 9.09 TB | 3.68 MB |
| 10.5 TB | 4.25 MB |
| 12.1 TB | 4.91 MB |
| 14 TB | 5.67 MB |
| 16.2 TB | 6.54 MB |
| 18.7 TB | 7.56 MB |
| 21.6 TB | 8.72 MB |
| 24.9 TB | 10.1 MB |
| 28.8 TB | 11.6 MB |
| 33.2 TB | 13.4 MB |
| 38.4 TB | 15.5 MB |
| 44.3 TB | 17.9 MB |
| 51.1 TB | 20.7 MB |
| 59.1 TB | 23.9 MB |
| 68.2 TB | 27.6 MB |
| 78.8 TB | 31.9 MB |
| 90.9 TB | 36.8 MB |
| 105 TB | 42.5 MB |
| 121 TB | 49.1 MB |
| 140 TB | 56.7 MB |
| 162 TB | 65.4 MB |
| 187 TB | 75.6 MB |
| 216 TB | 87.2 MB |
| 249 TB | 101 MB |
| 288 TB | 116 MB |
| 332 TB | 134 MB |
| 384 TB | 155 MB |
| 443 TB | 179 MB |
| 511 TB | 207 MB |
| 591 TB | 239 MB |
| 682 TB | 276 MB |
| 788 TB | 319 MB |
| 909 TB | 368 MB |
| 1,050 TB | 425 MB |
| 1,210 TB | 491 MB |
| 1,400 TB | 567 MB |
| 1,620 TB | 654 MB |
| 1,870 TB | 756 MB |
| 2,160 TB | 872 MB |
| 2,490 TB | 1,010 MB |
| 2,880 TB | 1.14 GB |
| 3,320 TB | 1.31 GB |
| 3,840 TB | 1.52 GB |
| 4,430 TB | 1.75 GB |
| 5,110 TB | 2.02 GB |
| 5,910 TB | 2.33 GB |
| 6,820 TB | 2.69 GB |
| 7,880 TB | 3.11 GB |
| 9,090 TB | 3.59 GB |
| 10,500 TB | 4.15 GB |
| 12,100 TB | 4.79 GB |
| 14,000 TB | 5.53 GB |
| 16,200 TB | 6.39 GB |
| 18,700 TB | 7.38 GB |
| 21,600 TB | 8.52 GB |
| 24,900 TB | 9.84 GB |
| 28,800 TB | 11.4 GB |
| 33,200 TB | 13.1 GB |
| 38,400 TB | 15.2 GB |
| 44,300 TB | 17.5 GB |
| 51,100 TB | 20.2 GB |
| 59,100 TB | 23.3 GB |
| 68,200 TB | 26.9 GB |
| 78,800 TB | 31.1 GB |
| 90,900 TB | 35.9 GB |
The usual answer is to lengthen the period, which lengthens detection lag, which tightens the inequality. There is no escape, only a choice of where to pay: scrubbing is a permanent tax proportional to the stored bytes.
The boundary of what detection buys
A checksum recovers nothing. It turns silent loss into loud loss. That is a real improvement: silent corruption spreads into backups, replicas and derived tables, while loud corruption stops at the reader. But the repair needs a second copy.
This is why ZFS, Ceph, HDFS and Colossus all pair checksums with automatic repair from a replica instead of offering detection alone.
Failure modes of the verifier
- A mismatch cannot tell you, from the inside, whether the data is corrupt, the checksum is corrupt, or the checksum code has a bug. So a system that panics on a mismatch can be taken down by a bug in its own integrity code.
- When two layers both checksum and they disagree, you get the "which layer is lying?" problem. It has the same shape as the unretryable
fsyncfailure from Group commit and fsync: the stack gives you no way to find which witness is at fault.
How the mechanisms of this article sit on three trade-offs
| Mechanism | Latency vs. throughput | Space vs. time | Durability vs. ack latency |
|---|---|---|---|
| Double-write buffer | sequential and batched; small latency cost | 2× data-page bytes | none: protects the meaning of an ack already given |
| Full-page writes | uses WAL bandwidth | up to ~80× on first touch | tied to the checkpoint interval |
| Device atomic writes | removes both costs | — | trades a mechanism for a promise you cannot verify |
| Battery-backed cache / PLP | 50–100× lower flush cost | — | moves the durability point; same ack, nearer |
| Persistent memory | flush becomes store + fence | — | atomic unit drops to 8 B; rewrites the protocol |
| Checksums | ~1–3% CPU on write and read | a few bytes per block | none: verifies a past ack |
| Scrubbing | permanent background read tax | detection lag versus bandwidth | none |