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
REPLICATIONandLOGIN, and restrict thepg_hba.confrecord to the backup host's address. - Stream, don't fetch — leave
--wal-method=streamon so recycled WAL cannot invalidate the backup. - Use a slot for standbys — a named slot with
--create-slotkeeps WAL available until the new standby connects. - Throttle on busy primaries — cap the rate with
--max-rateand prefer--checkpoint=spreadduring business hours. - Verify and test — run
pg_verifybackupon 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_basebackupwith streaming uses two WAL senders.
References
- PostgreSQL: pg_basebackup — Options, formats, WAL methods, compression, standby limits and version support.
- PostgreSQL: Continuous Archiving and Point-in-Time Recovery — Base backups, incremental backups and the recovery procedure.
- PostgreSQL: Progress Reporting — The
pg_stat_progress_basebackupview and its phases. - PostgreSQL: Replication settings —
max_wal_sendersand related sender and slot parameters. - PostgreSQL: pg_combinebackup — Reconstructing a full backup from an incremental chain.
- PostgreSQL: pg_verifybackup — Checking a backup against its manifest.
- PostgreSQL 10 Release Notes — WAL streaming made the
pg_basebackupdefault and the replication-friendly defaults. - PostgreSQL 17 Release Notes — Incremental backup support in
pg_basebackup. - Crunchy Data: Introduction to Postgres Backups — Logical versus physical backups and a worked
pg_basebackupcommand.