PostgreSQL has no idea where your WAL archive ends. During archive recovery it finds out the only way it can: it asks restore_command for the next segment, and the next, and the next, until the command fails. That failure is the end of the archive, by definition, whatever actually caused it. Nearly everything worth knowing about this parameter follows from that.
The default is the empty string. The context is sighup, and has been since PostgreSQL 14; in 12 and 13 it could only be set at server start, and before 12 it lived in recovery.conf, which was read once. Whether it is required depends on which signal file you created. With recovery.signal and no restore_command, the server refuses to start (must specify "restore_command" when standby mode is not enabled). With standby.signal it is optional, because a standby can stream from primary_conninfo instead; set neither and you get a warning and a standby that polls pg_wal forever, which is a configuration, not a plan.
The command runs under the shell, as the server’s OS user, with the data directory as its working directory, once per request (the same file can be asked for more than once). %f is the name of the file: usually a 16 MB segment, sometimes a timeline history file (the very first request a restore makes, with recovery_target_timeline at its default of latest, is for the history file of the timeline after the one it is on, to find out whether one exists). %p is where to put it, and it is the same path every time: pg_wal/RECOVERYXLOG for segments, pg_wal/RECOVERYHISTORY for history files. The server checks what arrived and renames it into pg_wal under its real name, so a long recovery never has more than one restored file staged. %r is the segment containing the last restartpoint, the earliest file the server would need if it were restarted now; it is all zeros whenever redo is not actually running (while the starting checkpoint is fetched, for history files, and for the re-read of the last segment at the end). It exists so a restore command can prune the archive as it goes, which is what pg_standby used to do; archive_cleanup_command is where that job lives now, and it gets its own %r. %% is a literal percent sign.
During archive recovery the archive is always asked first, even for segments already sitting in pg_wal. The base backup brought some WAL with it, and the server fetches those segments from the archive anyway, on the theory that the local copy might be a stale partial that was tarred up mid-write; in my test run the backup’s own pg_wal contained segment 000000010000000000000002, and the log still says restored log file "000000010000000000000002" from archive. Only when the archive does not have a file does the server try the local copy. A standby goes around a loop: archive, then pg_wal, then streaming, then a wal_retrieve_retry_interval nap (five seconds by default), then back to the archive. A standby with no primary_conninfo still visits the streaming state; it just fails instantly.
The archive ends wherever the shell says it does
The server reads the command’s exit status in three ways, plus one special case.
Exit status 0 means “the file is at %p.” The server believes you, then checks: the file has to exist and has to be exactly wal_segment_size bytes. A missing file produces a LOG line (could not stat file "pg_wal/RECOVERYXLOG", with the detail that restore_command returned zero anyway) and is then treated as not found. A wrong-sized file is FATAL in a point-in-time restore. In standby mode a file that is too small is assumed to be still copying and is retried on the next cycle, logged at DEBUG1; a file that is too large is fatal there too.
Exit status 1 through 125 means “not in the archive.” In standby mode that means “not yet,” and the loop continues. In a point-in-time restore it means recovery is over: redo done, pick a new timeline, open for writes.
Exit status 126 or 127 (the shell’s codes for “not executable” and “not found”), anything above 125 with one exception, or death by signal means “something is broken.” The server logs FATAL: could not restore file "…" from archive, the startup process exits, the postmaster shuts down, and pg_controldata still says in archive recovery, so you fix the command and start again from the last restartpoint. The exception is 143, which is how a shell reports a child killed by SIGTERM: it makes the startup process assume a shutdown is in progress and exit without logging anything at all. The only trace is startup process (PID n) exited with exit code 1. Do not pick that one. (The fatal boundary at 125 is the same one recovery_end_command uses; the 143 oddity is this parameter’s alone.)
Now look at the second category again, because it contains both “the archive has no such segment” and “the archive did not answer.” cp exits 1 for an I/O error, the same as for a missing file. aws s3 cp’s documented meaning for exit status 1 is that a transfer failed; it does not say why. Barman’s barman-wal-restore documents exit status 2 for “ssh connection FAILURE”. Every one of those is, to PostgreSQL, the end of the archive. If you set a recovery target, PostgreSQL 13 and later at least notice: recovery runs out of WAL short of the target and dies with FATAL: recovery ended before configured recovery target was reached, leaving the cluster in archive recovery so you can retry. (12 and earlier promoted silently; the recovery_target post has the details.) If you did not set a target, because the goal was “everything in the archive,” there is nothing to compare against, and the log of a restore that lost the last several hours is indistinguishable from the log of one that worked. Here is one, from a 43-segment archive whose restore_command returned 1 for a WAL segment exactly once, on the twenty-first:
1 LOG: restored log file "000000010000000000000014" from archive
2 LOG: redo done at 0/149A4388 system usage: CPU: user: 0.17 s, system: 0.33 s, elapsed: 1.22 s
3 LOG: last completed transaction was at log time 2026-10-27 05:00:28.654215-07
4 LOG: restored log file "000000010000000000000014" from archive
5 LOG: selected new timeline ID: 2
6 LOG: archive recovery complete
7 ...
8 LOG: database system is ready to accept connections
Twenty-two segments never got asked for. Nothing in that log is an error. [PLACEHOLDER: one-clause credential aside, e.g. “I have been handed a ‘successfully restored’ database that stopped four hours short for exactly this reason”; confirm, replace, or cut.]
There is a small accident in your favor on 15 and later, where recovery_prefetch is actually in effect (Linux, and macOS from 18, with maintenance_io_concurrency above zero). The first request for a segment usually comes from the read-ahead, and when that one fails the ordinary blocking read tries once more as soon as it has replayed what was already decoded; about ten milliseconds later, in my test. So a transient failure has to persist across two consecutive calls to end a restore. That is an implementation detail, not a retry policy, and with recovery_prefetch = off the first failure is the last, which is how the log above was produced.
The fix belongs in the command. A restore command that knows the difference between “the archive does not have this” and “I could not reach the archive” should exit 1 for the first and something above 127 for the second (255 is traditional; 126 and 127 would make the log claim the command was not found, and 143 you have just read about), so that the second is loud. WAL-G’s documentation makes the same point: wal-fetch exits 74 for a missing segment and 1 for other errors, and it tells you to wrap it in a script that turns the other errors into something in the 126 to 255 range. Whatever you use, when a restore finishes, compare the redo done at LSN in the log with the last segment you expected it to replay before you let anyone near the result.
One segment at a time
The other thing to know about restore_command is that it is serial and synchronous. The startup process calls system(), waits for the shell to come back, and never runs two at once; the read-ahead may ask for the next segment a little early, but it waits on that call just the same. Whatever your command’s fixed cost per call is, multiply it by the number of segments, because the calls never overlap.
On my test cluster, the replay phase of a 43-segment restore with a plain cp from a local directory took 3.1 seconds. The same restore with a 250 ms delay added to every call took 13.7 seconds, and while it ran the startup process sat in pg_stat_activity with wait_event_type = 'IPC' and wait_event = 'RestoreCommand' in 34 of 40 samples. That wait event exists in 15 and later, and it is the diagnostic: if the startup process spends its time there, the restore is bound by the fetch, not by replay, and faster storage will not help. Now scale it. A day of WAL on a busy system is easily 20,000 segments. At 250 ms of per-call overhead (an optimistic figure for aws s3 cp, which pays for a Python interpreter and a TLS handshake every time it runs) that is about 83 minutes of the startup process waiting on a shell, before a single byte is replayed.
The tools solve this by fetching ahead. pgBackRest does it with archive-async = y, which queues upcoming segments in spool-path so each archive-get call is served locally. WAL-G’s wal-fetch prefetches the segments after the one requested. barman-wal-restore --parallel N fetches the next N - 1 into a spool directory. The archive_command post already made the case that you should not be writing the archiving side yourself; the restore side is the same argument with a stopwatch attached.
Two smaller things. Because the context is sighup, a running restore can be pointed at a different archive, or switched to a faster fetcher, with a reload instead of a restart; and a standby whose archive host is gone, and whose cp says so in the log every five seconds, can be told to stop asking by setting restore_command = '' and reloading. It keeps streaming, and resetting the parameter starts the polling again. And since 13, pg_rewind -c (--restore-target-wal) reads the target cluster’s restore_command and uses it to fetch WAL that has already been recycled out of pg_wal, which turns a class of rewind failures into a slower rewind.
So: let the tool that wrote the archive read it back, with prefetching turned on. If the command is yours, make the three outcomes distinguishable, with exit 1 reserved for “the archive does not have it” and nothing else. And do not trust archive recovery complete until you have checked where it completed.