Skip to main content
RunBook Academy

PostgreSQLXIII · Backup, Archiving and Point-in-Time RecoveryBackup

WAL archiving: archive_command, integrity, retention

Advanced⏱ ~35 min🧪 Lab requiredpsql

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

Not yet marked complete on this device.

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
Read-only / Safethe cluster used for the measurements in this part
$ 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

Service impact possiblea genuine archive_command failure
$ 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/000000010000000000000001

The 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.

Read-only / Safethe view to alert on
$ 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+00

Alert 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 .backup label files, which record each backup’s start and end positions.
  • Every timeline history file. A .history file 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_command must return non-zero on failure, refuse to overwrite, be durable, be fast enough, and never hang.
  • Failures retry forever. pg_wal grows at the full generation rate until the command succeeds.
  • The DETAIL line in the log gives you the exact expanded command.
  • Alert on pg_stat_archiver.failed_count and on last_archived_time falling behind.
  • Check pg_settings.source. ALTER SYSTEM was 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_count and on archive lag, which is the failure that is silent until the volume is full.

Quiz

Knowledge check · 6 questions

  1. 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?

  2. Q2. Why does PostgreSQL retry a failing archive_command indefinitely rather than skipping the segment and moving on?

  3. 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?

  4. Q4. Which properties must a production archive_command have? Select all that apply.

  5. 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.

  6. 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.