recovery_init_sync_method decides how PostgreSQL does something most people do not know it does: after a crash, before it replays a single WAL record, it opens every file in the data directory and calls fsync() on it. The parameter chooses the technique. It does not offer a way out of the job, and the job is where a surprising share of your recovery time can go.

The default is fsync. The only other value is syncfs, which exists on Linux and nowhere else (elsewhere the value is simply not in the list, and the server will tell you so). The context is sighup. The parameter arrived in PostgreSQL 14; 13 and earlier have the fsync behavior and no switch.

Why the sync is there

The pass dates from May 2015, when commit 2ce439f3 (Abhijit Menon-Sen, reviewed by Andres Freund, committed by Robert Haas) went into every supported branch at once, as 9.4.2 and its siblings. The hazard it closes is narrow and nasty. When the server crashes, the kernel’s page cache holds writes PostgreSQL issued but had not yet synced; the checkpointer was going to get to them. Crash recovery replays the WAL from the last checkpoint and, at the end, checkpoints and syncs the files it touched; the older writes are not on its list, and they stay in the cache, pending, under a checkpoint that has now recycled the WAL that described them. If the machine then loses power, those pages are gone and nothing can rebuild them: newer data on disk, older data not, a database no replay can fix. The fix is blunt. If the control file says the last shutdown was not clean, sync everything before replay starts, so that whatever is in the cache is on the platter before recovery starts trusting it.

Two weeks later, 9.4.3 had to soften it. The first version treated any failure as fatal, which meant an unwritable SSL certificate in the data directory could keep a server from starting after a crash. Since then errors in this pass are logged and ignored, and that is still true: an I/O error from one of these fsync() calls produces a could not fsync file line at LOG and the server carries on into recovery. The data_sync_retry rule, that a failed fsync() is a PANIC and never a retry, does not apply to them. (It does apply to the sync_file_range() calls in the preliminary pass described below, which take the same PANIC path as everything else; but that call only starts writeback, and a failure there is a rarity.) (The pass is also skipped outright when fsync is off, which is one more reason to leave that alone.)

“Not clean” is broader than “crashed.” The test is the cluster-state field in pg_control: anything other than shut down or shut down in recovery triggers the pass. A fast or smart shutdown leaves one of those two. Everything else does not: pg_ctl stop -m immediate, kill -9, the OOM killer, a PANIC, a standby stopped with -m immediate, and, the one that catches people, the first start of every cluster built with pg_basebackup. The control file in a base backup is a copy of the primary’s, and the primary was in production, so a brand-new standby syncs its entire data directory before it can enter standby mode, right after pg_basebackup finished syncing the same files on the way in.

What it costs

With fsync, the startup process walks the data directory twice: once opening each file to ask the kernel to begin writeback (sync_file_range() on Linux), and again opening each file to fsync() it, then the directory it lives in. It follows the pg_wal and tablespace symlinks and no others. The cost is per file, not per byte, and most of your files are tiny. A table with a primary key and a text column is four files (heap, index, TOAST heap, TOAST index) before it holds a row.

I built a cluster of 150,000 such tables on 18.6, which is 601,112 files in 5.7 GB, checkpointed it, and killed the postmaster. The default method took 32 to 37 seconds to get through the pass with a cold cache and 29 with a warm one, and the difference was entirely in the first walk, which took five to nine seconds cold and one second warm. The second walk took 27 seconds either way: 45 microseconds per fsync() of a file with nothing to flush, on this VM’s local virtual disk, which is about as cheap as that call gets. The same cluster, copied with pg_basebackup and started as a standby, spent 33 seconds in the pass before entering standby mode. On storage where every uncached open() is a network round trip, the inode reads dominate and the numbers get much worse. The report that produced this parameter came from Discourse, who run one database per customer and had around 700,000 files in a data directory: the pass took seven minutes on a quiet server and ninety on a busy one with the same configuration, and the duration was unpredictable, which for a recovery phase is the worse property. Thomas Munro’s commit message describes field complaints of “minutes or even hours to begin crash recovery.”

Since 15 you can watch it happen. log_startup_progress_interval reports the pass, with the phase and the file it is on (every second here because I set it so; the default is ten):

1LOG: database system was interrupted; last known up at 2026-10-19 09:25:00 BST
2LOG: syncing data directory (pre-fsync), elapsed time: 1.00 s, current path: ./base/5/266395
3...
4LOG: syncing data directory (fsync), elapsed time: 1.00 s, current path: ./base/5/1186404
5...
6LOG: syncing data directory (fsync), elapsed time: 26.00 s, current path: ./base/5/695180
7LOG: database system was not properly shut down; automatic recovery in progress

On 14 there are no progress lines; the gap between database system was interrupted and automatic recovery in progress is the pass.

What syncfs does instead

Commit 61752afb (Thomas Munro, March 2021, for 14) added the alternative. syncfs() is a Linux system call that flushes an entire filesystem in one request, and the startup process makes it once for the data directory, once per tablespace, and once for pg_wal if that is a symlink. Nothing is opened per file; those directories are the only things the startup process touches. The 601,112-file cluster took two milliseconds, and a 201,020-file cluster I built on 19 RC1 went from 10.9 seconds to the same two milliseconds. The guarantee is unchanged: everything dirty on those filesystems reaches disk before replay begins.

The default stayed fsync because syncfs has two caveats, and both are smaller than they read in the documentation.

The first is that syncfs() flushes the whole filesystem, including dirty data that is not yours. If the data directory shares a filesystem with something that writes a great deal, that something gets flushed too, and you wait for it. I tried it: with 740 MB of an unrelated file sitting dirty in the cache, the pass took 600 milliseconds instead of two. That is a cost, but it is a sequential write of a bounded quantity (the kernel’s dirty_ratio caps how much can be waiting), paid on a machine that has just crashed, where the alternative is the per-file walk. On a server whose data filesystem holds nothing but PostgreSQL there is nothing else to flush, and that describes every production database server I am prepared to defend.

The second is that on kernels before Linux 5.8 (August 2020), syncfs() does not reliably report writeback errors; a failed write shows up in dmesg and nowhere else. On 5.8 and later it returns them the way fsync() does. Keep this in proportion: an error in this pass is logged and ignored under either method, so what a pre-5.8 kernel costs you is a LOG line, not a safety check. If you are on RHEL 8 or one of its clones, which ship 4.18, whether the fix was backported is a question for Red Hat.

Setting it before you need it

The context is sighup because Justin Pryzby pointed out, before 14 shipped, that a parameter consulted only at the start of crash recovery should not need a restart to change. It is read at the moment the pass begins, so ALTER SYSTEM SET recovery_init_sync_method = 'syncfs' plus a reload arms it for the next crash with no restart at all. The context matters more than it looks, because the most common crash does not restart the postmaster: a backend is OOM-killed or segfaults, the postmaster logs all server processes terminated; reinitializing, and a new startup process runs the pass with whatever values the postmaster holds, which under the original postmaster context would have been the values from the last restart. I did the reload on the running 601,112-file cluster, killed one backend with SIGKILL, and the reinitialization synced in four milliseconds. This is a parameter you set on a Tuesday afternoon for a crash on a Saturday night.

If you did not, and you are watching the progress lines crawl through ./base, there is a way out, but it is not the obvious one. pg_ctl stop -m fast does not interrupt the pass. The startup process notes the shutdown request and keeps syncing; the request takes effect only once recovery proper has started, which is after the pass finishes. On my cluster the fast shutdown took 25 seconds to complete, having waited for the sync it was supposed to be cancelling. pg_ctl stop -m immediate does interrupt it, and because the control file is not touched until the pass is over, the cluster state is still in production afterwards: the restart repeats the pass from the beginning, with whatever method you give it, and the log says nothing scarier than database system was interrupted. So the emergency sequence is an immediate stop, then pg_ctl start -o '-c recovery_init_sync_method=syncfs', and then, once it is up, the ALTER SYSTEM you should have run on Tuesday.

Since 17 the client tools (initdb, pg_basebackup, pg_checksums, pg_combinebackup, pg_dump, pg_rewind, pg_upgrade) take --sync-method=syncfs for their own final sync. pg_basebackup --sync-method=syncfs speeds up the backup; the standby’s first start is still governed by the server parameter, so set both.

So: on Linux with a kernel from this decade, set syncfs and reload, on every cluster, primaries and standbys alike. The guarantee does not change, the cost drops from a handful of system calls per file to one per filesystem, and the file count that makes this matter (partitioning, one schema per tenant, one database per customer) only ever goes up. Leave fsync where the kernel is older than 5.8 or the data directory shares a filesystem with something you do not control. For everyone else, the only argument for the default is that it is the default, and that argument gets weaker with every table you create.