How Postgres recovery_prefetch Can Turn a Replica Blip Into an Outage
PostgreSQL's recovery_prefetch, on by default since version 15, can lengthen write outages on databases with a single synchronous standby. If the standby's WAL receiver drops, it must replay its backlog before streaming again, and the primary's commits wait. Prefetch can multiply the standby's random read IOPS, stretching that catch-up from minutes to much longer.
What recovery_prefetch is
PostgreSQL 15 added WAL prefetching during recovery, credited in the release notes to Thomas Munro and controlled by the recovery_prefetch setting. While replaying WAL, the server looks ahead for blocks that the upcoming records will touch and that are not yet in shared buffers, and issues read-ahead advice to the operating system so the reads are in flight before replay needs them. The documented values are off, on and try. The default is try, which enables prefetching only where the OS supports read-ahead advice, so on a typical Linux build it is effectively on.
Two other settings bound it: wal_decode_buffer_size (default 512kB, start-time only) limits how far ahead it looks, and maintenance_io_concurrency limits how many prefetches are in flight. The pg_stat_recovery_prefetch view reports what it is doing.
The intent is sound. On a replica that is IO bound on random reads, overlapping those reads can make replay much faster. The question raised this week is what happens when the assumptions behind it do not hold.
The reported failure
On October 20, 2026, Jeremy Schneider published "Postgres recovery_prefetch Considered Harmful and Causing Outages" on his blog, Ardent Performance Computing. His claim: on streaming replicas, especially with synchronous replication, prefetch can turn a routine connection blip into a long write outage on the primary.
The chain
With synchronous replication, the primary waits for a synchronous standby to confirm WAL before a commit returns. If the only synchronous standby's WAL receiver stops (a network hang, a killed process), the standby falls back to replaying WAL it can read from its archive or local pg_wal. Schneider points to the recovery state machine in xlogrecovery.c and writes that a standby in that mode "will not go back to streaming until it's caught up." Until it streams again, it cannot acknowledge anything, so commits on the primary hang. How long they hang is set by how fast the standby replays the backlog.
Where prefetch makes it worse
Schneider says that when the primary does large sequential IO, recovery_prefetch "converts those operations into 8k random reads" on the replica, amplifying IOPS by up to 16x for the same bytes read. He also cites two earlier warnings. Stephen Frost raised concern on pgsql-hackers in February 2021 about "unnecessary prefetches for blocks we've already sent a prefetch for". Michael Zhilin of Postgres Pro reported in March 2025 that a bare-metal customer had hours of replay lag with high kernel CPU, resolved by disabling recovery_prefetch, and noted that prefetching "doesn't consider that shared buffer may be loaded from WAL FPI rather than from disk". Full page images are written by default, which is why Schneider calls the benefit "a little dubious" in common configurations.
The reproduction
Schneider published a lab reproduction (the ardentperf-labs walreceiver-outage-repro repository on GitHub). The setup was small on purpose: a cloud disk limited to 500 IOPS, 2 CPUs, 8 GB of memory, one synchronous standby, pgbench at scale 640 (about 10 GB) with autovacuum disabled on pgbench_accounts, running a steady 5 transactions per second. He ran a burst that updated 5 percent of pgbench_accounts, then killed the WAL receiver.
With prefetch at its default, throughput dropped to zero and writes hung for over 30 minutes. The primary did about 300 IOPS for around 5 minutes. The replica sat at its 500 IOPS ceiling for about 40 minutes, reading roughly the same number of bytes as the primary in far more operations. With recovery_prefetch = off and full page writes still on, the read IOPS burst disappeared and the standby applied the update in under 10 minutes.
Read it with the right confidence. It is one workload on deliberately constrained hardware, and the author describes his recommendation as tentative and the discussion as ongoing. It is also a clean demonstration of a mechanism that matches an independent production report, which is enough to act on defensively.
Are you exposed?
The risky shape is a primary that requires one specific synchronous standby, with that standby on storage that has a hard IOPS ceiling, running PostgreSQL 15 or later with recovery_prefetch at try or on. Check synchronous_standby_names on the primary: a single name, or FIRST 1 with only one healthy candidate, means one standby stalling stops writes. Check SHOW recovery_prefetch on each standby. Compare the standby's provisioned IOPS with the primary's: a replica sized for streaming steady state can be badly undersized for catch-up replay.
Then measure rather than guess. On the standby, pg_stat_recovery_prefetch shows how many blocks were prefetched versus skipped because they were already cached, recently prefetched, or covered by a full page image. On the primary, pg_stat_replication shows sync_state and replay_lag per standby, and on the standby pg_stat_wal_receiver shows whether the receiver is streaming at all.
Defensive options and their trade-offs
Disable prefetch on synchronous standbys. Set recovery_prefetch = off, after testing replay speed on your own workload. Check the setting's context in pg_settings to see whether a reload is enough on your version. The cost: workloads that genuinely benefit, for example random-read-heavy replay where full page images do not cover most blocks, lose that speedup, and the setting also applies to crash and archive recovery.
Stop depending on one standby. Two standbys with quorum commit, synchronous_standby_names = 'ANY 1 (s1, s2)', means a single stalled standby no longer blocks writes. Schneider recommends considering this. The cost is another replica and its storage.
Size replica storage for catch-up, not steady state. IOPS headroom on the standby shortens every recovery path, with or without prefetch.
Alert on the state, not only the lag. Page when the synchronous standby is not in streaming state or when commits on the primary start waiting on sync replication, not just when replay lag crosses a threshold.
Write the emergency step down. Clearing synchronous_standby_names and reloading restores writes immediately, at the price of commits in that window not being guaranteed on a standby. That is a durability decision with an owner, so it belongs in a runbook with a named approver, not in someone's memory at 3am.
Frequently asked questions
- What does recovery_prefetch do in PostgreSQL?
- Added in PostgreSQL 15, recovery_prefetch makes the server look ahead in the WAL during recovery and issue read-ahead advice for blocks not yet in shared buffers, so reads overlap with replay. Values are off, on and try; the default try enables it where the operating system supports read-ahead advice.
- How can recovery_prefetch cause an outage?
- Per Jeremy Schneider's October 2026 analysis, if a synchronous standby's WAL receiver stops, the standby replays its backlog before resuming streaming, and the primary's commits wait. Prefetch can convert large sequential reads into many 8k random reads, saturating replica IOPS and stretching that catch-up, and therefore the write outage, from minutes to much longer.
- Should I turn off recovery_prefetch?
- Schneider leans towards disabling it but calls that tentative. A defensible approach is to measure with pg_stat_recovery_prefetch and replay tests, disable it on synchronous standbys with constrained IOPS if replay is not faster with it on, and remove single-standby synchronous setups with quorum commit regardless of the prefetch decision.
- Does quorum commit fix the problem?
- It removes the outage path rather than the slow replay. With synchronous_standby_names set to ANY 1 across two standbys, one standby stalling in catch-up no longer blocks commits, because the other can acknowledge. The trade-off is the cost of an extra replica, and both standbys still need enough IOPS to recover.