WAL archiving and point-in-time recovery

PostgreSQL · Expert · 15 min

PITR is how you recover from 'someone ran DELETE without WHERE at 14:03' with minutes of data loss.

Every change is written to the write-ahead log (WAL) before data pages, in 16 MB segment files. Checkpoints periodically flush dirty pages so older WAL can be recycled. That same log enables point-in-time recovery (PITR):

1. Take a base backup, a physical copy of the data directory made while the server runs: pg_basebackup -D /backups/base -Ft -z -X stream. PostgreSQL 17 adds incremental base backups (--incremental, combined later with pg_combinebackup). 2. Archive WAL continuously: set wal_level = replica, archive_mode = on and an archive_command (or archive_library) that copies each finished segment to safe storage. 3. To recover, restore the base backup, set restore_command to fetch archived segments, set recovery_target_time = '2026-05-03 14:02:00+00', create recovery.signal, and start the server. It replays WAL up to the target and stops, then you promote it.

Tools such as pgBackRest, Barman and WAL-G handle the details: parallel and incremental backups, compression, encryption, retention, and verification. Use one rather than hand-written scripts.

Key settings: archive_timeout bounds how much WAL (and so data) sits un-archived on a quiet server. Monitor pg_stat_archiver for failures — a failing archive_command makes WAL pile up until the disk fills.

Physical backups are tied to the major version and the whole cluster; keep logical dumps too for portability.

Example

# postgresql.conf
#   wal_level = replica
#   archive_mode = on
#   archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'
#   archive_timeout = 60

pg_basebackup -h db1 -U replicator -D /backups/base_2026_05_03 -Ft -z -X stream -P

# --- disaster: bad DELETE at 14:03, restore to 14:02 on a new server ---
tar -xzf /backups/base_2026_05_03/base.tar.gz -C /var/lib/postgresql/18/main
cat >> /var/lib/postgresql/18/main/postgresql.auto.conf <<'EOF'
restore_command = 'cp /archive/%f %p'
recovery_target_time = '2026-05-03 14:02:00+00'
recovery_target_action = 'pause'
EOF
touch /var/lib/postgresql/18/main/recovery.signal
pg_ctl -D /var/lib/postgresql/18/main start
# inspect the data, then: SELECT pg_wal_replay_resume(); to finish and promote

Remember

  • Base backup + archived WAL = restore to any point in time
  • Use pgBackRest, Barman or WAL-G rather than custom scripts
  • Monitor pg_stat_archiver; a broken archive_command fills the disk

Watch out for

  • Archiving WAL without ever doing a test recovery to a target time.

Check yourself

  1. What does a PITR restore need besides the base backup?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.postgresql.org/docs/current/continuous-archiving.html