PostgreSQL Physical Backup Guide with pg_basebackup

PostgreSQL Physical Backups with pg_basebackup

Seeding a new streaming standby and starting a point-in-time recovery chain are the same job at the first step: copy the whole cluster, consistently, while it keeps serving traffic. pg_basebackup does that over a normal replication connection, with no downtime and no filesystem snapshot, and it leaves the server in the same state it found it.

Purpose and Overview

pg_basebackup takes a base backup of a running cluster. The backup is taken without affecting other clients of the database, and the server is put into and out of backup mode automatically. The output serves two purposes: it is the starting point for a log-shipping or streaming-replication standby, and it is the base that point-in-time recovery (PITR) replays archived WAL on top of.

The scope is always the entire cluster. Every database, every tablespace and the configuration files in the data directory come along; there is no way to pick one database or one table. For a single database, a selective restore, or a copy that loads into a newer major version, use a logical dump instead. The step-by-step version is in backing up a remote PostgreSQL database with pg_dump. Physical and logical backups complement each other: the physical one restores fast and supports PITR, the logical one is portable.

The connection runs over the replication protocol. That needs a role with the REPLICATION attribute (or a superuser), a matching replication record in pg_hba.conf, and enough WAL sender slots on the server.

Sample Code

 1# Compressed tar format: archive and PITR base
 2pg_basebackup \
 3  --host=db1.example.com \
 4  --port=5432 \
 5  --username=replicator \
 6  --pgdata=/var/backups/pgbase_$(date +%Y%m%d) \
 7  --format=tar \
 8  --gzip \
 9  --wal-method=stream \
10  --checkpoint=fast \
11  --progress \
12  --verbose
13
14# Plain format with recovery settings: seed a standby
15pg_basebackup \
16  --host=db1.example.com \
17  --port=5432 \
18  --username=replicator \
19  --pgdata=/var/lib/postgresql/standby \
20  --format=plain \
21  --wal-method=stream \
22  --write-recovery-conf \
23  --slot=standby1 \
24  --create-slot \
25  --checkpoint=fast \
26  --progress

Role and pg_hba.conf entry on the primary:

1CREATE ROLE replicator REPLICATION LOGIN PASSWORD 'change_me';
1host  replication  replicator  192.0.2.10/32  scram-sha-256

Progress from a second session on the server being backed up:

1SELECT pid,
2       phase,
3       backup_total,
4       backup_streamed,
5       tablespaces_total,
6       tablespaces_streamed,
7       round(100.0 * backup_streamed / NULLIF(backup_total, 0), 1) AS pct_complete
8FROM pg_stat_progress_basebackup;

Notes: pg_basebackup can work with servers of the same or older major version, down to 9.1; --wal-method is the PostgreSQL 10 name of the option (earlier releases called it --xlog-method). Streaming WAL needs server 9.3 or later and tar format needs 9.5 or later. pg_stat_progress_basebackup exists from PostgreSQL 13. The backup directory must be empty or absent; pg_basebackup creates it, and missing parent directories, if needed. The hostname, address and password above are placeholders.

Code Breakdown

Each option in the two commands controls one of four things: where the files go, what shape they take, how WAL is captured, and how hard the backup leans on the primary.

Format and destination

--pgdata (-D) is required. With --format=plain (the default) the files land with the same layout as the source data directory, ready to start a server. Plain format puts additional tablespaces at the same absolute path they have on the source, so a plain backup run on the same host as the server fails when tablespaces are in use unless --tablespace-mapping relocates them. With --format=tar the main data directory becomes base.tar, each other tablespace gets its own tar named after its OID, and you unpack them yourself before starting a server.

Compression

--gzip (-z) compresses tar output and adds the .gz suffix to every tar file. Plain format cannot use client-side compression. --compress offers finer control: gzip, lz4 or zstd, a level, and a choice of client or server side, as in --compress=server-zstd:5. Compressing on the server cuts transfer bandwidth but spends server CPU, which matters on a busy primary.

WAL method

--wal-method=stream (the default) opens a second connection and streams WAL while the files copy, so the backup is self-contained and needs no extra WAL retained on the source if the client keeps up. In tar format the WAL arrives as pg_wal.tar. fetch collects WAL at the end, so wal_keep_size must be large enough that nothing is recycled first; if it was, the backup fails and is unusable. none leaves WAL out entirely, which only works if the WAL archive supplies it.

Replication slot

Streaming uses a temporary replication slot automatically (server 10 or later) unless --no-slot is given. --slot names a permanent slot and --create-slot creates it before the copy. A named slot stops the primary from removing WAL between the end of the backup and the moment the new standby connects, provided the standby uses the same name as primary_slot_name.

Checkpoint and throttle

The backup begins with a checkpoint on the source. The default is spread, during which pg_basebackup looks idle; --checkpoint=fast forces an immediate one at the cost of a short I/O burst. --max-rate caps the transfer rate in kilobytes per second (an M suffix means megabytes), with valid values from 32 kB/s to 1024 MB/s. It always limits the data directory and limits WAL only under fetch.

Recovery settings

--write-recovery-conf (-R) creates standby.signal and appends the connection settings, plus the slot name if one was given, to postgresql.auto.conf in the target directory. That is the whole difference between a plain copy and a ready-to-start standby.

Key Backup Concepts

Connection slots

The server must allow at least one WAL sender for the backup plus one for WAL streaming. max_wal_senders defaults to 10 on current releases and can only be changed with a restart. Set it slightly higher than the number of expected clients, so a client that drops can reconnect while its old connection is still held.

wal_level

The default wal_level is replica, which writes enough WAL for archiving and replication. minimal is not sufficient for streaming clients, and max_wal_senders itself requires replica or higher before standbys can connect.

Checksums and the manifest

Data page checksums are verified during the backup by default, if they are enabled on the server. A failure gives a non-zero exit status, but the backup directory is left in place, and failures are also reported in pg_stat_database. --no-verify-checksums turns this off. Checksum verification during streaming base backups arrived in PostgreSQL 11. The server also sends a backup manifest, a list of every file with size, modification time and a checksum (CRC32C unless --manifest-checksums says otherwise, with SHA algorithms available for tamper detection).

Backups from a standby

A standby can be the source if it accepts replication connections (max_wal_senders, hot_standby, pg_hba.conf) and full_page_writes is on at the primary. Limits apply: the standby cannot be forced to switch WAL at the end, so with -X none a quiet primary may leave the backup waiting for the last segment (pg_switch_wal on the primary clears it), and a promotion during the backup makes it fail.

Practical Applications

Building a streaming standby

Run the second command from the sample code on the standby host, start the server on the resulting directory, and it connects to the primary using the settings -R recorded. With a named slot, the primary keeps the WAL the standby still needs while it catches up.

A PITR base backup

For PITR, take the tar-format backup and keep it together with the continuously archived WAL (archive_mode on, a working archive_command). Restoring means: stop the server, copy the existing data directory aside if space allows, empty the data directory and tablespace directories, unpack the backup as the database system user (not root), clear pg_wal, set restore_command and any recovery target in postgresql.conf, create recovery.signal, and start the server. When recovery finishes the server removes recovery.signal and opens for normal use. The interval between base backups is a trade-off: more time between them means more archived WAL to keep and replay.

Verifying before you rely on it

pg_verifybackup checks a backup against its manifest. It handles plain and tar formats, including compressed tar. WAL parsing works only for plain-format backups, so for a tar backup add -n (--no-parse-wal). Verification does not replace a test restore, which is the only proof the backup starts and holds the data you expect.

Incremental backups on PostgreSQL 17

Starting with PostgreSQL 17, pg_basebackup --incremental=<manifest of the earlier backup> stores only blocks changed since that backup. The server needs summarize_wal = on (default off) so WAL summaries exist in pg_wal/summaries covering the span between the two backups; if they are missing or removed in time (see wal_summary_keep_time), the incremental backup fails. An incremental backup cannot be started directly. Run pg_combinebackup with the full backup and every incremental in order, oldest first, and an output directory (-o) to produce a synthetic full backup to restore from. Deleting any link in the chain makes the later backups unrestorable.

Version Compatibility

pg_basebackup can back up servers as old as 9.1. PostgreSQL 10 renamed --xlog-method to --wal-method and made stream the default, and also raised the defaults for wal_level (now replica) and max_wal_senders. PostgreSQL 11 added checksum verification during the backup. PostgreSQL 13 added the pg_stat_progress_basebackup view, the backup manifest, and pg_verifybackup.

PostgreSQL 15 added more compression choices, including Zstandard and server-side compression during pg_basebackup. PostgreSQL 17 added incremental backup and pg_combinebackup. The client and server versions need not match: the tool works against servers of the same or older major version, but --incremental requires a server on 17 or later.

Pre-13 servers have no progress view; use --progress on the client side instead. Without a manifest on those servers, a restore test is the only check.

Best Practices

  • Dedicated replication role — grant only REPLICATION and LOGIN, and restrict the pg_hba.conf record to the backup host's address.
  • Stream, don't fetch — leave --wal-method=stream on so recycled WAL cannot invalidate the backup.
  • Use a slot for standbys — a named slot with --create-slot keeps WAL available until the new standby connects.
  • Throttle on busy primaries — cap the rate with --max-rate and prefer --checkpoint=spread during business hours.
  • Verify and test — run pg_verifybackup on every backup and do periodic full restores to a scratch host.
  • Keep the chain intact — when using incrementals, store the full backup, every increment and the manifests together, and never prune a middle link.
  • Watch the slot count — one running pg_basebackup with streaming uses two WAL senders.

References

Posts in this series