Restore PostgreSQL WAL Safely with pg_getwal
You will use Debian's pg_getwal helper to copy one PostgreSQL WAL segment from a pg_receivewal archive into the path PostgreSQL requests. The same command handles plain, gzip-compressed, lz4-compressed and still-growing partial archive files. Allow about fifteen minutes for a read-only test with a small fixture, or longer if you are checking a live recovery configuration.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide describes postgresql-common version 257build1.1, the version installed on this machine. The installed script is at /usr/share/postgresql-common/pg_getwal; it is not available as pg_getwal through this shell's normal PATH. You need a readable WAL archive, a writable destination for the requested segment, and the PostgreSQL tools that created the archive. The examples do not start, stop or reconfigure a cluster.
Safety boundary
A restore command writes the destination path supplied by PostgreSQL. Test with a disposable directory first. Do not point an experiment at a live data directory, and do not replace an existing recovery setting without a rollback copy and a maintenance plan.
1. Confirm the installed helper
Check the package and script as an ordinary user. No elevated privilege is needed if you can read the package metadata:
$ dpkg-query -W -f='${Package} ${Version}\n' postgresql-common
postgresql-common 257build1.1
$ test -x /usr/share/postgresql-common/pg_getwal && echo installed
installed
The manpage gives the command shape as two arguments:
$ /usr/share/postgresql-common/pg_getwal /path/to/wal/%f %p
%f and %p are PostgreSQL placeholders. PostgreSQL substitutes %f with the requested WAL file name and %p with the destination path before invoking the command. If you run the helper by hand, pass real paths instead. There is no documented option switch to add.
2. Test a plain WAL file without touching a cluster
Create a small fixture in a temporary directory. This checks path handling and copying, not WAL validity:
$ work=$(mktemp -d /tmp/pg-getwal.XXXXXX)
$ mkdir -p "$work/archive" "$work/out"
$ printf 'fixture-wal\n' > "$work/archive/000000010000000000000001"
$ /usr/share/postgresql-common/pg_getwal \
"$work/archive/000000010000000000000001" "$work/out/segment"
$ cat "$work/out/segment"
fixture-wal
A successful exit status and matching content show that the helper found a plain archive file and copied it to the second argument. The first argument's directory must already exist. The helper does not create it for you.
Checkpoint
Confirm the output exists before using a real archive:
$ test -s "$work/out/segment" && echo 'destination is non-empty'
destination is non-empty
3. Exercise compressed and partial archives
The helper first looks for the requested path with .gz, then .lz4, then the uncompressed path. It can also use .gz.partial, .lz4.partial and .partial. The compression tools must be installed when their format is used.
Here is a safe gzip fixture. Notice that the requested base path does not itself exist:
$ printf 'compressed-wal\n' | gzip > "$work/archive/000000010000000000000002.gz"
$ /usr/share/postgresql-common/pg_getwal \
"$work/archive/000000010000000000000002" "$work/out/compressed"
$ cat "$work/out/compressed"
compressed-wal
A partial archive is copied as partial data for the plain .partial form:
$ printf 'partial-wal\n' > "$work/archive/000000010000000000000003.partial"
$ /usr/share/postgresql-common/pg_getwal \
"$work/archive/000000010000000000000003" "$work/out/partial"
$ cmp "$work/archive/000000010000000000000003.partial" "$work/out/partial"
$ echo 'partial fixture matches'
partial fixture matches
For compressed partial files, the script expands the destination to the configured WAL segment size. It reads PG_VERSION in the current directory and invokes the matching PostgreSQL pg_controldata, so that branch belongs to a real PostgreSQL archive layout. Do not fake that layout in production, and do not treat a successful copy of a fixture as proof that a partial segment is usable for recovery.
4. Use it as a restore_command
In a PostgreSQL recovery configuration, the practical form is a path template followed by PostgreSQL's destination placeholder:
restore_command = '/usr/share/postgresql-common/pg_getwal /var/backups/postgresql/wal/%f %p'
That setting is operational configuration and may affect recovery. Edit it only through the cluster's normal configuration workflow, preserve the previous value, and reload or restart only according to your recovery procedure. The pg_restorecluster tool can set this command when archive recovery is configured, using the backup layout it manages. If you hand-write the setting, check that the archive directory is the one produced by your pg_receivewal or backup workflow.
Do not quote the placeholders separately or substitute a fixed WAL name. PostgreSQL must be able to request different segments. Keep the archive path controlled by the administrator: the helper passes the second argument to cp, gunzip, unlz4 or truncate, so an unexpected restore destination is a configuration problem, not a reason to grant broad write access.
5. Read failures without hiding them
A missing WAL file is deliberately quiet and returns status 1, because recovery may ask for a segment that has not arrived yet. Reproduce that distinction in the fixture:
$ /usr/share/postgresql-common/pg_getwal \
"$work/archive/not-present" "$work/out/missing"
$ printf 'status: %s\n' "$?"
status: 1
$ test ! -e "$work/out/missing" && echo 'no destination was created'
no destination was created
Do not turn every status 1 into a generic permission alert without checking the archive. A missing file can be a normal wait during streaming, but it can also mean that retention removed a segment too early or that the template points at the wrong directory.
Other failures are reported with a non-zero status. A missing archive directory returns status 129. Decompression, copying, metadata lookup or truncation failures also return status 129. Capture the status immediately and inspect the relevant paths:
$ test -d /var/backups/postgresql/wal && echo 'archive directory exists'
$ test -w /var/lib/postgresql/15/main/pg_wal && echo 'destination is writable'
$ printf 'permission check status: %s\n' "$?"
The second check above is only a read-only test of permissions. It is not a substitute for checking the exact %p path in PostgreSQL logs. If the archive or destination requires elevated access, use the service account and permissions designed for that cluster rather than making the helper world-writable or running recovery as root.
Done means
- You confirmed the installed package version and absolute helper path.
- A plain fixture was copied to a separate destination and verified.
- You understand that compressed and partial suffixes are selected automatically.
- Your restore template leaves
%fand%pfor PostgreSQL to substitute. - A missing archive file is recognised as status 1, while operational failures remain visible.
- Any real configuration change has a saved previous value and a tested recovery or rollback plan.