PostgreSQLXIII · Backup, Archiving and Point-in-Time RecoveryBackup
WAL archiving: archive_command, integrity, retention
What you'll learn
- Write an archive_command that cannot silently destroy the archive
- Diagnose an archiving failure from pg_stat_archiver and the server log
- Explain why a failing archive threatens the primary, not just the backup
- Set a retention policy that keeps the archive restorable
Prerequisites
Practice
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
A base backup is one instant. The archive is what turns it into every
instant since. Lesson XII-03 covered archive_mode as a WAL retention
mechanism; this lesson covers it as the recovery mechanism it exists
for.
The configuration
wal_level = replica # or logical; minimal cannot be archived
archive_mode = on # requires a restart
archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'
archive_timeout = 60 # optional; bounds worst-case data loss
$ SELECT name, setting FROM pg_settings WHERE name LIKE 'archive%' OR name = 'wal_level'; archive_command | test ! -f /archive/%f && cp %p /archive/%f
archive_mode | on
archive_timeout | 60
wal_level | replica%p is the path of the segment to archive, relative to PGDATA. %f
is its bare filename. The test ! -f half is not decoration: it refuses
to overwrite a file that already exists. Without it, a re-archived
segment overwrites a good one, and the archive is silently broken at
exactly the point you will need it.
When it fails
$ docker logs rbpg-pitr 2>&1 | grep -A2 'archive command failed'cp: cannot create regular file '/archive/000000010000000000000001': No such file or directory
2026-08-27 21:22:04.341 UTC [62] LOG: archive command failed with exit code 1
2026-08-27 21:22:04.343 UTC [62] DETAIL: The failed archive command was: test ! -f /archive/000000010000000000000001 && cp pg_wal/000000010000000000000001 /archive/000000010000000000000001The DETAIL line reports the exact expanded command, with %f and %p
substituted. That is what makes an archive failure diagnosable: you can
copy the line, run it by hand, and see the real error.
PostgreSQL retries a failing archive_command indefinitely. It does
not give up and it does not skip the segment. Skipping would silently
break the archive, so refusing to skip is correct — and the consequence
is that the segment cannot be recycled and pg_wal grows at the full
generation rate until the command succeeds.
This is the mechanism behind one of the most common PostgreSQL outages:
a full pg_wal filesystem caused by an archive destination that went
away. Lesson XII-03 measured the same growth from a different cause.
Part XVIII covers the recovery.
$ SELECT * FROM pg_stat_archiver;-[ RECORD 1 ]------+------------------------------
archived_count | 1
last_archived_wal | 000000010000000000000021
last_archived_time | 2026-08-27 21:46:03.786349+00
failed_count | 0
last_failed_wal |
last_failed_time |
stats_reset | 2026-08-27 21:41:03.608505+00Alert on failed_count increasing, and on last_archived_time falling
behind. A stalled archive on a quiet cluster looks identical to a
healthy one until you compare it against the clock.
-- how far behind is the archive?
SELECT last_archived_wal,
now() - last_archived_time AS archive_age,
failed_count,
now() - last_failed_time AS since_last_failure
FROM pg_stat_archiver;
The precedence trap
Retention
The archive has to contain enough to reach every point you promise to recover to. That means, at minimum:
- Every segment from the start LSN of the oldest base backup you intend to keep onward. Not the oldest segment you happen to have — the one that backup needs.
- The
.backuplabel files, which record each backup’s start and end positions. - Every timeline history file. A
.historyfile is tiny and losing one makes the timeline it describes unreachable.
# Bounded by the oldest backup, not by a fixed age.
# Deleting segments a surviving backup needs makes that backup useless.
pg_archivecleanup /archive 000000010000000000000009
pg_archivecleanup deletes segments older than the one named. The name
to pass comes from the oldest base backup you are keeping, and this is
the ordering that matters: expire the backup first, then trim the
archive to the new oldest backup. Reversing those two steps deletes
WAL a live backup depends on.
What to take from this
archive_commandmust return non-zero on failure, refuse to overwrite, be durable, be fast enough, and never hang.- Failures retry forever.
pg_walgrows at the full generation rate until the command succeeds. - The
DETAILline in the log gives you the exact expanded command. - Alert on
pg_stat_archiver.failed_countand onlast_archived_timefalling behind. - Check
pg_settings.source.ALTER SYSTEMwas measured writing the file, reporting success, and changing nothing. - Retention is bounded by the oldest backup you keep, not by an age.
Cross-course references
- Linux for Production Sysadmins — Part XXXVI (Scheduled operations) covers the archive destination’s own retention, and Part XIV (Filesystems) covers what happens to the primary when that destination stops accepting writes.
- Observability for Production Sysadmins — Part LIX (Database
observability) covers alerting on
failed_countand on archive lag, which is the failure that is silent until the volume is full.
Quiz
Knowledge check · 6 questions
Q1. An operator runs ALTER SYSTEM SET archive_command and pg_reload_conf on a containerised cluster. Both succeed and postgresql.auto.conf contains the new value, but archiving behaviour is unchanged. What should they check?
Q2. Why does PostgreSQL retry a failing archive_command indefinitely rather than skipping the segment and moving on?
Q3. A retention job deletes archived WAL older than 30 days. The weekly full backup has failed silently for four weeks. What is the recovery window this morning?
Q4. Which properties must a production archive_command have? Select all that apply.
Q5. pg_receivewal can achieve a smaller recovery point objective than archive_command because it streams WAL continuously rather than waiting for a segment to fill.
Q6. An archive destination becomes unreachable at 02:00. Trace what happens to the primary over the following hours.
Passing score: 75%. Answers are checked in this browser.