Ask a database what it fears most and it won't say a crashed transaction — the WAL handles that. It will say a page write that stopped halfway. PostgreSQL's docs spell out the arithmetic: disk sectors are commonly 512 bytes, and "PostgreSQL typically writes 8192 bytes, or 16 sectors, at a time." Power can fail after sector 9 of 16. InnoDB has it worse: its default page is 16 KB sitting on filesystems that mostly use 4 KB blocks, so one logical page write is several physical ones. The result of a mid-write crash is a page that is neither the old version nor the new one — a torn page — and, counterintuitively, the crash-recovery log you already paid for cannot fix it.
MySQL and PostgreSQL solve this identical problem in two completely different places, with two completely different bills. Understanding why tells you a lot about both engines — and about when you're paying for protection your storage already gives you.
Why the redo log can't save you
Both engines already write a log before touching data files, so why isn't recovery just "replay the log"?
Because neither log records whole pages by default. InnoDB's redo log is physiological: it records operations against page numbers ("apply this change to page N"), not complete page images. Replaying an operation requires the page underneath to be internally consistent — either fully old or fully new. A torn page is neither, so redo apply has nothing sound to apply against. PostgreSQL's documentation says the same thing about its own WAL: "The row-level change data normally stored in WAL will not be enough to completely restore such a page during post-crash recovery."
So both systems need a source of known-good full pages to fall back on. That's the fork in the road.
InnoDB: write everything twice
InnoDB's answer is the doublewrite buffer. From the MySQL manual:
The doublewrite buffer is a storage area where InnoDB writes pages flushed from the buffer pool before writing the pages to their proper positions in the InnoDB data files. If there is an operating system, storage subsystem, or unexpected mysqld process exit in the middle of a page write, InnoDB can find a good copy of the page from the doublewrite buffer during crash recovery.
The flush path becomes: write a batch of dirty pages sequentially into the doublewrite area, fsync(), then write each page to its real location in the tablespace, fsync() again. In older MySQL versions that staging area lived inside the system tablespace; in current versions it lives in dedicated files next to your data, named by page size:
#ib_16384_0.dblwr
#ib_16384_1.dblwrThere are at least two of them per buffer pool instance — one for flush-list pages, one for LRU-list pages — up to a maximum of twice the number of buffer pool instances.
Recovery logic is a tidy case analysis. If a page copy is torn inside the doublewrite files, the crash happened during the staging write — the real tablespace page is untouched, so the copy is simply discarded. If a page is torn in the tablespace, the staging write must have completed first, so the good copy is restored from the doublewrite files. There is no crash point that leaves both copies bad.
The name suggests a 2x write penalty; the reality is milder. The manual is explicit: "Although data is written twice, the doublewrite buffer does not require twice as much I/O overhead or twice as many I/O operations," because the staging write is one large sequential chunk with a single fsync() (unless innodb_flush_method=O_DIRECT_NO_FSYNC). Percona's long-standing guidance puts the expected cost at no more than 5–10%.
Modern MySQL also has a middle setting worth knowing:
SET GLOBAL innodb_doublewrite = 'DETECT_ONLY';ON/DETECT_AND_RECOVER (the default) gives full protection. DETECT_ONLY writes only metadata — torn pages are detected at recovery but not repaired. You can switch dynamically among the enabled modes, but moving to or from OFF requires a restart.
PostgreSQL: photograph the page into the WAL
PostgreSQL refuses to maintain a second staging area. Instead, with full_page_writes = on (the default), it "writes the entire content of each disk page to WAL during the first modification of that page after a checkpoint." Recovery always starts from a checkpoint, so replay can begin from that full-page image — a photograph of the page in a known-good state — and apply the compact row-level records on top. A torn page in the data file simply gets overwritten wholesale.
The cost structure is the interesting part. InnoDB pays a roughly constant tax on every flush. PostgreSQL pays a spiky one: immediately after a checkpoint, nearly every page touched is being touched for the first time since that checkpoint, so the WAL fills with 8 KB images instead of small records. WAL volume follows a sawtooth synced to your checkpoint schedule. That produces a genuinely counterintuitive tuning rule, straight from the docs: "one way to reduce the cost of full-page writes is to increase the checkpoint interval parameters." Aggressive checkpointing — often tuned for faster recovery — makes you re-photograph hot pages more often.
The second lever is wal_compression (default off), which compresses exactly these full-page images using pglz, lz4, or zstd, trading CPU for WAL volume. A related knob, wal_log_hints, forces full-page images even for hint-bit-only changes — and is implied when data checksums are enabled.
Turning full_page_writes off is legal and fast, and the docs are blunt about the price: it "might lead to either unrecoverable data corruption, or silent data corruption, after a system failure," with risks "similar to turning off fsync, though smaller." Silent is the operative word — nothing errors at the moment of tearing; you find out later, or never.
The same insurance, different premiums
| InnoDB doublewrite | Postgres full-page writes | |
|---|---|---|
| Good copy lives in | Dedicated .dblwr files | The WAL stream |
| Cost shape | Steady (~5–10%) on data flushes | Sawtooth WAL inflation after each checkpoint |
| Main tuning lever | innodb_doublewrite mode | Checkpoint distance, wal_compression |
| Side effect | Extra fsync in the flush path | Bigger WAL: slower archiving, more replication traffic |
Neither choice is free, and each leaks into different subsystems. Postgres's tax inflates everything downstream of the WAL — archiving, streaming replication, PITR storage. InnoDB's tax sits squarely on the buffer-pool flush path, which is why write-heavy benchmarks feel it.
When the hardware makes it redundant
Both mechanisms exist because a multi-sector write isn't atomic. Make it atomic and the insurance is pure overhead.
- Copy-on-write filesystems. ZFS never overwrites a block in place, so a torn in-place write can't happen. The PostgreSQL docs name it directly: "If you have file-system software that prevents partial page writes (e.g., ZFS), you can turn off this page imaging."
- Atomic-write storage. MySQL has long auto-disabled doublewrite on Fusion-io devices with atomic write support. The mainstream version of this is Amazon RDS Optimized Writes: on Nitro-based instance classes, "the database can write 16-KiB pages directly to data files reliably and durably in one step," so RDS runs MySQL 8.0.30+ with
innodb_doublewriteset toFALSEand claims "up to two times higher write transaction throughput." (There's a catch that tells you how deep this reaches: instances created before the feature's November 2022 release have an incompatible file system configuration and need a blue/green migration to adopt it.)
One caution from the Postgres reliability docs before you generalize: a battery-backed RAID controller does not count unless it guarantees data reaches the cache as full 8 KB pages. Surviving power loss is not the same as surviving it on a page boundary.
The doublewrite buffer and full-page writes are both workarounds for a promise storage historically refused to make. As atomic 16 KB writes stop being exotic, expect the most interesting setting on a new database host to be the one that lets you stop paying for 1990s disk semantics.
