Skip to main content
RunBook Academy

← All runbooks in PostgreSQL

medium riskservice affecting~40 min

Runbook: Take a Physical Base Backup

1 · Prerequisites

Confirm every item is in place before any state change.

  • A replication role, and a pg_hba.conf rule that admits it for the replication pseudo-database from the host taking the backup
  • Destination storage with room for the whole cluster, measured with pg_database_size across all databases plus a margin, not estimated
  • Confirmation that WAL archiving is configured and currently succeeding, because a base backup without the WAL that follows it can only be restored to the moment the backup ended
  • Knowledge of the cluster wal_level, which must be replica or logical for a base backup to be usable
  • A change note recording why this backup is being taken and how long it is to be retained
  • Agreement on whether the backup runs against the primary or a standby

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · Confirm archiving is healthy before you start. SELECT archived_count, last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time FROM pg_stat_archiver; A base backup taken while archiving is broken is a backup whose recovery window ends where the archive stopped.
  • · **Confirm wal_level.** SHOW wal_level; must be replica or logical. At minimal the WAL does not contain enough information to restore from a base backup, and pg_basebackup will refuse.
  • · **Confirm max_wal_senders has a free slot.** SELECT setting FROM pg_settings WHERE name = 'max_wal_senders'; and SELECT count(*) FROM pg_stat_replication;. A base backup consumes a sender, and on a cluster with standbys the limit is often already close.
  • · Measure the size. SELECT pg_size_pretty(sum(pg_database_size(datname))) FROM pg_database; plus the size of pg_wal. Then df -h on the destination. A backup that fills the destination halfway through wastes the whole run.
  • · Decide the checkpoint mode before running the command. With the default spread checkpoint, pg_basebackup waits for a checkpoint paced over checkpoint_completion_target x checkpoint_timeout — on a default cluster, up to 270 seconds of apparently doing nothing. -c fast requests an immediate checkpoint instead.
  • · Decide whether to use a replication slot. -C -S backup_slot creates a temporary slot so the required WAL is retained for the duration. Without one, a busy cluster can recycle a segment the backup still needs. With one, a failed backup can leave a slot behind that retains WAL indefinitely.
  • · Confirm the destination directory is empty, and that it is not inside the data directory of a running cluster.

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1Record the starting position. SELECT pg_current_wal_lsn(), now(); on the source. This is the lower bound of what the backup will cover and it belongs in the change note.
  2. 2Run the backup with an immediate checkpoint and progress reporting. pg_basebackup -h <source> -U repl -D /backup/base-$(date -u +%Y%m%dT%H%M%SZ) -c fast -X stream -P --checkpoint=fast. The -c fast is what turns a five-minute pause into a few seconds.
  3. 3Choose the WAL method deliberately. -X stream opens a second connection and streams WAL alongside the backup, so the result is self-contained and restorable without the archive. -X none relies entirely on the archive, and is only safe when you have verified the archive covers the whole window.
  4. 4Watch progress rather than waiting blind. -P prints the byte count and tablespace progress. On the source, SELECT pid, state, sent_lsn, backend_start FROM pg_stat_replication; shows the backup as a sender.
  5. 5Capture the exit status without a pipe. pg_basebackup ...; echo "exit=$?". As with any backup command, a pipeline discards the status of everything but the last command.
  6. 6Record the ending position. SELECT pg_current_wal_lsn(), now(); on the source, immediately after the backup completes. The pair of LSNs and timestamps defines the window this backup plus the archive can recover across.
  7. 7**Read backup_label in the destination.** It records the checkpoint LSN, the backup start time, and the WAL segment recovery must begin from. This file is what makes the copy a backup rather than a directory of files, and it is deleted automatically when recovery completes.
  8. 8Confirm the WAL the backup needs has been archived. SELECT pg_switch_wal(); on the source, then check pg_stat_archiver.last_archived_wal has advanced past the segment named in backup_label. Without this, the backup cannot reach a consistent state on restore.
  9. 9**If a temporary slot was created with -C, confirm it was removed.** SELECT slot_name, active, wal_status FROM pg_replication_slots; A slot left behind by a failed backup retains WAL until somebody drops it, and that is how a WAL volume fills.
  10. 10Record the backup size and duration, and compare the size against the source. A base backup dramatically smaller than the cluster is a truncated backup, and it exits zero.
  11. 11Verify the backup by restoring it, on a schedule if not on every run. See the restore runbook; a base backup that has never been started is a directory, not a recovery capability.

4 · Verification

Confirm the procedure actually fixed the problem.

  • pg_basebackup exited zero, read without a pipe.
  • The destination contains backup_label, PG_VERSION, base/, global/ and pg_wal/. A missing backup_label means this is a copy of a data directory, not a base backup.
  • The backup size is within a sensible margin of the source cluster size. Record both numbers so the comparison is possible later.
  • The WAL segment named in backup_label has been archived: compare it against pg_stat_archiver.last_archived_wal.
  • pg_stat_archiver.failed_count did not increase during the backup window.
  • No replication slot was left behind: SELECT slot_name, active, wal_status FROM pg_replication_slots;
  • The starting and ending LSNs and timestamps are recorded, so the recovery window this backup supports is written down rather than inferred later.

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • A base backup reads the source and does not modify it, so there is nothing to roll back on the cluster itself beyond the artefacts the run creates.
  • If the backup failed partway, delete the incomplete destination directory before retrying. A partial directory that looks plausible is a hazard, and pg_basebackup will refuse to write into a non-empty directory anyway.
  • If a replication slot was created and the backup failed, drop it: SELECT pg_drop_replication_slot('backup_slot');. An abandoned slot retains WAL indefinitely and will eventually fill the WAL volume.
  • If the backup consumed a WAL sender slot that a standby needed, confirm the standby reconnected: SELECT application_name, state FROM pg_stat_replication;
  • If the backup is being discarded, remove it from every location it was copied to. A base backup is a complete copy of the cluster, including every role's password verifier.
  • If the backup was taken to enable a change that has been abandoned, keep it until the decision is final and then remove it with a note recording the decision.

6 · Escalation

When the runbook isn't enough, contact:

  • · pg_basebackup fails with a WAL segment that has already been removed: escalate and re-run with a replication slot. This means the cluster recycled a segment the backup still needed, and it will recur without a slot.
  • · Archiving is failing and cannot be fixed quickly: escalate before taking the backup, or take it with -X stream and record explicitly that its recovery window ends at the backup itself. A base backup with no following WAL cannot perform point-in-time recovery.
  • · The backup is markedly smaller than the source cluster: escalate rather than accepting it. A truncated backup exits zero and is indistinguishable from a good one by size alone unless somebody compares.
  • · The backup takes long enough to affect production: escalate to whoever owns the maintenance window and consider running it against a standby instead, which removes the load from the primary.
  • · max_wal_senders has no free slot and increasing it requires a restart: escalate to the change owner. Taking a standby offline to free a slot is a worse trade than waiting for a window.
  • · This is the estate's first physical backup and no restore has ever been performed: escalate to the service owner. An untested backup is a plan, and the difference between a plan and a capability is measured in hours during an outage.

A base backup is a byte-level copy of the cluster plus a backup_label that says which WAL record recovery must start from. It is worth roughly nothing on its own and everything in combination with the WAL that follows it.

Which is why half of this runbook is about the archive.

The five-minute pause that is not a hang

Blast radius

ActionReversible?What it costs if wrong
pg_basebackup against the primaryYes — it only readsI/O and one WAL sender slot for its duration
-c fastYesA burst of checkpoint I/O on the source
-C -S slotYes, drop the slotA slot left behind by a failed run retains WAL forever
-X none with a broken archiveNoA backup that cannot reach consistency on restore
Copying the backup somewhere convenientNoA complete copy of the cluster, including every password verifier

Two LSNs define what this backup is worth

Record both, on the source, around the run:

SELECT pg_current_wal_lsn(), now();   -- before
-- ... pg_basebackup ...
SELECT pg_current_wal_lsn(), now();   -- after

The backup can be restored to any moment from the second of these onwards, provided the archive is continuous from the segment named in backup_label. Without the archive, it restores to exactly one moment: the end of the backup.

Confirm the archive covers the backup

-- force the current segment out
SELECT pg_switch_wal();

-- then confirm the archiver has caught up past what backup_label names
SELECT last_archived_wal, last_archived_time, failed_count
FROM pg_stat_archiver;

The size check nobody does

SELECT pg_size_pretty(sum(pg_database_size(datname))) FROM pg_database;

Compare that against du -sh on the destination. A truncated base backup exits zero and contains a plausible directory structure. Size is the only cheap check that catches it, and it only works if the source size was recorded at the same time.

Finally

A base backup that has never been started is a directory. Restore one on a schedule, time it, and write the number down — that number is the recovery time objective you actually have, as distinct from the one in the document.

References

  1. PostgreSQL 18 documentation, Continuous Archiving and Point-in-Time Recovery
  2. PostgreSQL 18 documentation, pg_basebackup
  3. PostgreSQL 18 documentation, pg_stat_archiver
  4. PostgreSQL 18 documentation, Replication Slots