full_page_writes is a boolean, it defaults to on, and its context is sighup, set in postgresql.conf or on the command line. It is the reason crash recovery works at all, and it is also the reason your WAL graph has a sawtooth in it.

The problem it solves

A PostgreSQL page is 8kB. The sectors underneath it are not: 512 bytes traditionally, 4kB on modern drives, some other size on whatever your storage layer actually does when nobody is looking. An 8kB page write is therefore not one operation but several, and a crash can land in the middle of it. What survives is a page holding some sectors of the new version and some of the old, which is what everyone calls a torn page.

The obvious question is why the WAL cannot simply fix that during recovery, given that the WAL is a complete record of every change. The answer is the part worth understanding, because it explains why the fix has to be as blunt as it is.

Why replaying the WAL is not enough

PostgreSQL’s WAL records are not byte-level diffs. They are physiological: a record says something closer to “delete tuple 7” than “overwrite bytes 2048 through 2071 with the following.” To apply a record like that, recovery has to read the existing page, verify it, consult its line pointer array to find out what tuple 7 currently is, and then modify it.

Every one of those steps assumes the page it is reading is coherent. A torn page is not. Its checksum fails if checksums are on, and if they are off it is worse, because the structures recovery needs to consult are a mixture of two different versions of the page. The delta is perfectly correct and has nothing valid to apply itself to.

So PostgreSQL switches strategies for the first modification of each page after a checkpoint: instead of logging the change, it logs the entire page. Recovery then does not patch that page, it replaces it wholesale with a known-good image and replays subsequent changes on top. Because replay always begins at a checkpoint, the first touch after each checkpoint is the only one that needs this treatment, which is the single detail that keeps the whole scheme affordable.

What it costs, and the tuning trap

It costs WAL volume, and it costs it in a distinctive shape. Immediately after every checkpoint, each page touched for the first time drags a full 8kB into the WAL. Traffic spikes, throughput dips, and the effect decays as the working set gets touched. Run a long benchmark and you can see it plainly: TPS moves in waves synchronized to the checkpoint interval.

This produces a tuning trap that catches people going the other direction. The instinct when checkpoints cause I/O spikes is to make them more frequent so each one does less work. But more checkpoints means more first-touches, which means more full page images, and the documentation says so directly: a smaller checkpoint interval increases the volume of WAL output, partially negating the goal of using a smaller interval. The lever that reduces the cost of full page writes is a longer checkpoint interval, not a shorter one.

The other lever is wal_compression, which exists more or less precisely for this. Compressing full page images trades some CPU during logging and replay for a smaller WAL, which is usually a good trade when full page images dominate your WAL volume, and lz4 or zstd make it a cheaper one than the original pglz did.

When off is defensible

There is a real answer here, and it is narrow. Turning full_page_writes off is safe when the storage underneath guarantees that an 8kB page write is atomic: it either lands entirely or not at all. Copy-on-write filesystems are the canonical case, ZFS above all, where a record is never overwritten in place and a recordsize that is a multiple of 8kB means an 8kB page cannot be split across two separately-written records. Enterprise SAN arrays with battery-backed cache and an array block size of 8kB or more make a similar claim, as does some flash with an advertised atomic write unit.

The trouble is the standard of proof. The documentation’s phrasing for the failure mode is “unrecoverable data corruption, or silent data corruption,” and the second one is the problem. This does not fail loudly at the moment you get it wrong. It fails later, quietly, in a page that reads back without complaint and contains the wrong bytes.

And knowing the guarantee holds all the way down is harder than it sounds. The PostgreSQL wiki’s page on atomic I/O is largely a list of unresolved questions about whether an aligned 8kB write can be relied upon to reach the device as one command rather than being split somewhere in the filesystem, volume manager, or driver. The blunt community annotation on the parameter is that ZFS users claim to be able to turn this off, but that it has not been destruction-tested. If you cannot describe, layer by layer, why a torn write is impossible on your stack, you do not have the guarantee. You have a belief about it.

The exception nobody expects

Here is the wrinkle that surprises even people who have earned the right to turn it off. Two tools still need full page writes regardless of your setting: pg_basebackup silently enables them for the duration of the backup, and pg_rewind refuses to run without them.

The reason is that these tools need a different kind of atomicity than the one your filesystem is providing. Copy-on-write protects you against a page torn by a crash during a write. These tools read data files while the server is concurrently writing to them, so their exposure is a torn read, which is a separate hazard that the filesystem’s write atomicity does not address. As Andres Freund put it in the discussion of relaxing the pg_rewind restriction, this is not about torn pages in case of crashes but about reading pages while they are being written to.

So even on storage where off is genuinely correct, the setting has a floor under it that the backup and rewind paths impose on your behalf.

Leave it on

The documentation puts this parameter in the same bracket as fsync, with the risks described as similar though smaller, and advises turning it off only under the same circumstances. That comparison is right about the severity and slightly misleading about the character. Running fsync = off through a power failure announces itself: the cluster does not come back, and you go get the backup. Running full_page_writes = off on storage that turns out to tear can hand you a database that starts cleanly, answers queries, and is wrong in one page you will find in six months.

Leave it on. If full page images are genuinely dominating your WAL, lengthen the checkpoint interval and turn on wal_compression, which are the two levers that reduce the cost without giving up the guarantee.

Related