SimpleBackupsSimpleBackups

pgBackRest: how to back up, restore and recover PostgreSQL

Posted on

A 400 GB PostgreSQL cluster and a nightly pg_dump is a combination that works right up until the moment it doesn't. The dump takes four hours. The restore takes longer, because rebuilding indexes is slower than reading them. And whatever happened at 14:32 this afternoon, you are going back to 03:00 this morning, because that is the only state you have.

Both of those problems have the same root. A logical dump reconstructs your database by replaying SQL, and it captures exactly one moment: the one it started at.

pgBackRest solves both, and it is the reason most large self-hosted PostgreSQL installations end up running it. This guide covers what it does, how to configure it from a blank server, and the parts where it stops.

What pgBackRest does that pg_dump cannot

PostgreSQL has two kinds of backup, and nearly every decision downstream follows from which one you are taking.

A logical backup is what pg_dump and pg_restore produce: your schema and data expressed as SQL and data streams. It is portable, so you can restore it into a newer major version or a different provider entirely, and you can pull back a single table from it. It is also slow to restore on large databases, and it captures a single instant.

A physical backup is a byte-level copy of the PostgreSQL data directory. It restores by putting files back where they were, so it is fast, and it is tied to the same major version and platform. On its own, a physical backup is still a single instant.

What turns a physical backup into something better is the Write-Ahead Log. PostgreSQL writes every change into the WAL before applying it to the data files. Archive that log continuously alongside a base backup, and you can restore the base backup and then replay the log forward to any moment you choose. That is point-in-time recovery, and it is the only native route to a recovery point measured in seconds rather than hours.

pgBackRest is the tool that manages all of this: it takes the base backups, it receives the archived WAL, it stores both in a repository you configure, it expires old backups on a schedule, and it drives the restore. PostgreSQL supplies the primitives, documented in the continuous archiving chapter of the manual, and pgBackRest is the layer that makes them practical to operate.

Why trust this article

We run PostgreSQL backups every day. The pattern we see most often is a team whose backups are fine and whose restores have never been attempted, which is a different thing from being backed up. Everything below is written to be run, not skimmed.

Two things are worth settling before you invest a day in this.

The project is current. Version 2.59.0 was released on 20 July 2026 and added PostgreSQL 19 support, and the project supports ten PostgreSQL versions at a time. There has been public discussion about the project's leadership; releases have continued through it.

It does not replace your logical dumps. A pgBackRest backup cannot be restored into a different major version, which is exactly the job pg_dump exists for. Most clusters we see running pgBackRest also run a periodic logical dump, for major-version upgrades, for moving to another provider, and for the table-level recovery that physical backups cannot do.

Installation, and the permissions that trip people up

pgBackRest is packaged in the PostgreSQL Global Development Group repositories, which is the route to prefer over building from source. If you already installed PostgreSQL from PGDG, the repository is configured.

On Debian and Ubuntu:

sudo apt update
sudo apt install pgbackrest

On RHEL, Rocky and AlmaLinux:

sudo dnf install pgbackrest

Confirm the version, because the configuration options below assume a recent one:

pgbackrest version

One decision belongs here rather than later, because changing it afterwards means moving a repository. pgBackRest can run in two shapes. In the simple one, it runs on the database host and writes to a repository it can reach directly, whether that is local disk or object storage. In the other, it runs on a dedicated repository host and connects to the database servers over TLS or SSH, pulling backups to a machine the database has no write access to.

The second shape costs an extra server and buys something specific: a database host that is compromised cannot delete the backups, because it never had permission to. If ransomware is in your threat model rather than only hardware failure, that separation is the point. Everything in this guide works in either shape; the examples use the simpler one.

pgBackRest runs as the postgres user, the same account that owns the data directory. It needs a configuration file and somewhere to write logs, and both need the right ownership:

sudo mkdir -p -m 770 /var/log/pgbackrest
sudo chown postgres:postgres /var/log/pgbackrest
sudo mkdir -p /etc/pgbackrest
sudo touch /etc/pgbackrest/pgbackrest.conf
sudo chmod 640 /etc/pgbackrest/pgbackrest.conf
sudo chown postgres:postgres /etc/pgbackrest/pgbackrest.conf

Permissions are the single most common reason a first pgBackRest run fails, and the error messages point at the symptom rather than the cause. Every path pgBackRest touches, the config file, the log directory and the repository, has to be readable and writable by postgres. If a command fails with a permission error, check ownership before you check your configuration.

The stanza, and why configuration starts there

Most tutorials skip past the word "stanza" as though it were jargon for "config section." It is worth ten seconds of attention, because it is the concept the rest of the tool is organised around.

A stanza binds one PostgreSQL cluster to one repository. It carries the path to the data directory, the connection details, and where the backups and WAL for that cluster are stored. Every pgBackRest command takes a --stanza argument, and running several clusters means running several stanzas.

Name it after the cluster rather than the environment or the hostname. A stanza called prod-db-01 becomes misleading the day you fail over to prod-db-02 and restore the same backups there; a stanza called billing does not.

Here is a minimal configuration with a local repository:

[global]
repo1-path=/var/lib/pgbackrest
repo1-retention-full=2
start-fast=y
log-level-console=info
log-level-file=detail

[billing]
pg1-path=/var/lib/postgresql/17/main

[global] holds settings that apply everywhere, [billing] is the stanza. start-fast=y tells PostgreSQL to force a checkpoint immediately rather than waiting for one, so the backup starts now instead of in a few minutes.

Next, tell PostgreSQL to archive its WAL to pgBackRest. In postgresql.conf:

wal_level = replica
archive_mode = on
archive_command = 'pgbackrest --stanza=billing archive-push %p'
max_wal_senders = 3

archive_mode and wal_level both require a restart, not a reload. Plan for that, because it is the one step in this guide that costs you downtime.

sudo systemctl restart postgresql

Now create the stanza and verify the whole path works:

sudo -u postgres pgbackrest --stanza=billing stanza-create
sudo -u postgres pgbackrest --stanza=billing check

check is the step to care about. It confirms that pgBackRest can reach the cluster, that the repository is writable, and that WAL archiving actually completes end to end by forcing a segment switch. A green check means the configuration is real. Do not take your first backup until it passes.

If check hangs rather than failing, the usual cause is archive_command succeeding at the shell level while pgBackRest cannot write to the repository, so PostgreSQL retries forever and the WAL directory grows. Watch pg_wal during your first run. A steadily growing pg_wal on a cluster with archiving enabled is an incident in slow motion, because a full disk stops the database.

Configuring the repository: local, NFS and S3

The repository is where backups and archived WAL live. Where you put it determines what the backup actually protects you against.

A repository on the same host as the database protects you against a dropped table and nothing else. Not disk failure, not a deleted instance, not a compromised host. It is a reasonable place to start while you are learning the tool and a poor place to stay.

RepositoryProtects againstDoes not protect against
Local diskAccidental data loss, bad migrationsHost loss, disk failure, ransomware
NFS or separate volumeThe above, plus disk failureLoss of the site or the account
Object storage (S3, Azure, GCS)The above, plus site lossNothing material, if credentials are scoped correctly

For object storage, replace the repository block:

[global]
repo1-type=s3
repo1-path=/billing
repo1-s3-bucket=acme-pg-backups
repo1-s3-endpoint=s3.eu-west-1.amazonaws.com
repo1-s3-region=eu-west-1
repo1-s3-key=AKIAEXAMPLEKEYID
repo1-s3-key-secret=examplesecretkeyvalue
repo1-retention-full=4

The same shape works for any S3-compatible provider by pointing repo1-s3-endpoint at it, which is how you would target Cloudflare R2, Backblaze B2 or Wasabi. Azure Blob Storage and Google Cloud Storage have their own repo1-type values. Our walkthrough of getting Postgres backups into S3 covers the bucket and credential side in more detail.

Two settings belong here rather than in a later hardening pass.

Encrypt the repository. pgBackRest encrypts client-side, so the provider never sees plaintext:

repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=a-long-random-passphrase-you-store-elsewhere

Set this before the first backup. Changing the cipher passphrase later means starting a new repository, because existing backups were encrypted with the old one.

Store the passphrase somewhere other than the server. If the only copy of repo1-cipher-pass is in pgbackrest.conf on the host you are protecting, an encrypted off-site backup and no backup at all are the same thing on the day the host is gone.

You can also define repo2-* alongside repo1-* to write the same backups to two repositories, which is the straightforward way to satisfy the off-site half of a 3-2-1 policy without a second tool.

Full, differential and incremental backups

pgBackRest takes three types of backup, and the difference is what each one depends on.

sudo -u postgres pgbackrest --stanza=billing --type=full backup
sudo -u postgres pgbackrest --stanza=billing --type=diff backup
sudo -u postgres pgbackrest --stanza=billing --type=incr backup
TypeContainsRestore needsTypical use
FullEvery file in the clusterItself onlyWeekly baseline
DifferentialChanges since the last fullThe full, plus this oneDaily
IncrementalChanges since the previous backup of any typeThe full, plus every incremental sinceHourly

The trade is straightforward. Incrementals are the cheapest to take and the most fragile to restore, because they form a chain and every link has to be present. Differentials grow larger as the week goes on but depend on one other backup. Fulls are expensive and depend on nothing.

Recent versions also support block-level incremental backup, which stores only the changed blocks within a file rather than the whole file. On a large database where a few rows change in a big table, the difference is substantial. [verify] the exact option name and minimum version against the release notes for the version you install before documenting it internally.

Check what you have with:

sudo -u postgres pgbackrest --stanza=billing info

That output lists every backup, its type, its timestamp, its size, and the WAL range it covers. It is the command to reach for first when something looks wrong, and the one to put in front of an auditor who asks what your retention actually looks like in practice.

Expiring a full backup expires everything that depends on it. If your retention keeps two fulls and you take hourly incrementals, the moment the older full is expired, every incremental chained to it becomes unrestorable. This is not a bug, but it does surprise people who reason about retention as "we keep 30 days of backups" without accounting for the dependency chain.

Restoring a cluster

A backup you have not restored is an assumption. This is the section to rehearse on a scratch server before you need it.

The basic restore stops PostgreSQL and puts files back:

sudo systemctl stop postgresql
sudo -u postgres pgbackrest --stanza=billing restore
sudo systemctl start postgresql

pgBackRest expects the data directory to be empty. When it is not, the restore fails rather than overwriting, which is deliberate.

For a data directory that already has files, --delta compares checksums and replaces only what differs:

sudo -u postgres pgbackrest --stanza=billing --delta restore

On a large cluster where most files are unchanged, a delta restore is dramatically faster than a full one, and it is the option to reach for when you are recovering a replica or rolling back a failed upgrade. It is also the one worth practising, because it is the one you will want under time pressure.

To restore onto a different server, install pgBackRest there, give it a configuration pointing at the same repository and stanza, and run the restore. Nothing about the backup is tied to the original host. This is the property that makes physical backups useful for standing up replicas, and it is also why repository credentials deserve the same care as database credentials.

To restore a specific backup rather than the latest:

sudo -u postgres pgbackrest --stanza=billing --set=20260812-030014F --delta restore

Take the set label from info output.

Whatever the restore path, the step most people skip is checking the result. A restore that returns exit code zero has told you the files were written, not that the database is correct. Three checks take a minute and are worth building into the runbook.

First, confirm the cluster actually finished recovery and is accepting writes rather than sitting in recovery mode:

sudo -u postgres psql -c "SELECT pg_is_in_recovery();"

Second, confirm the data is the shape you expect. Row counts on two or three tables you know well will catch a restore that landed at the wrong point in time, which is the failure mode that otherwise goes unnoticed for days:

sudo -u postgres psql -d billing -c \
  "SELECT count(*) FROM invoices; SELECT max(created_at) FROM invoices;"

That max(created_at) is the useful one. It tells you, in one number, roughly where in time the restored database actually stopped.

Third, check the PostgreSQL log for the recovery messages. A restore that quietly ran out of archived WAL and stopped earlier than you asked reports it there and nowhere else.

Restoring the whole cluster is the normal case. Restoring one database out of several is possible with --db-include, and the caveat is significant: the databases you exclude are replaced with stubs, so the cluster starts but those databases are unusable and have to be dropped. If what you actually need is one table back, that is a job for a logical dump and pg_restore, not for pgBackRest.

Point-in-time recovery, step by step

This is what the WAL archiving was for. Someone ran a DELETE without a WHERE clause at 14:32, and you want the cluster as it was at 14:31.

sudo systemctl stop postgresql
sudo -u postgres pgbackrest --stanza=billing \
  --type=time \
  --target="2026-08-13 14:31:00+00" \
  --delta \
  restore
sudo systemctl start postgresql

pgBackRest selects a base backup taken before your target, restores it, and writes the recovery settings PostgreSQL needs so that on startup it replays archived WAL forward and stops at the target. Watch the PostgreSQL log during startup; recovery progress and the point where it stops are reported there.

Other target types are available: --type=xid to stop at a transaction ID, --type=lsn for a log sequence number, --type=name for a named restore point you created earlier with pg_create_restore_point(), and --type=immediate to stop as soon as the cluster is consistent.

Three things determine whether this works when you need it.

Your recovery window is bounded by retention. You can recover to any moment covered by a backup you still hold plus its WAL. Expire the WAL and the window closes, no matter how much disk you have.

Timezones are a real hazard. Specify an offset in the target, as above. A target interpreted in the server's local time when you meant UTC recovers to the wrong hour, and you will not notice until you check the data.

By default the cluster is promoted and a new timeline begins. Once you promote, the recovered cluster diverges from the original. If you are unsure whether 14:31 is the right target, restore to a scratch server first and look at the data before you promote anything in production.

Practise this once, on a copy, before you need it. The command is short, and the parts that go wrong are the parts you cannot see: an ambiguous timezone, a WAL segment that was never archived, a repository the standby cannot reach. None of those show up in a backup report, and all of them show up during a recovery.

Retention, expiration and verifying a backup

Retention in pgBackRest is expressed in counts of full backups, not in days, which trips people up when they translate a policy written as "90 days."

[global]
repo1-retention-full=4
repo1-retention-diff=6
repo1-retention-archive-type=full
repo1-retention-archive=4
SettingControls
repo1-retention-fullHow many full backups to keep. Everything depending on an expired full expires with it.
repo1-retention-full-typeWhether the number above means a count of backups or a number of days
repo1-retention-diffHow many differentials to keep
repo1-retention-archiveHow much archived WAL to keep, which is what bounds your PITR window
repo1-retention-archive-typeWhich backup type the WAL retention is anchored to

Expiration runs automatically after a backup, and you can run it directly:

sudo -u postgres pgbackrest --stanza=billing expire

[verify] these option names against the user guide for your installed version before committing them to a runbook; retention options have gained members over time.

Then there is the command most setups never run:

sudo -u postgres pgbackrest --stanza=billing verify

verify walks the repository and checks that backups and WAL are internally consistent and match their recorded checksums. It catches silent corruption and truncated uploads, which is exactly the class of problem that stays invisible until a restore.

Worth being precise about what it does and does not prove. verify tells you the files in the repository are intact. It does not tell you that PostgreSQL will start from them, that your application will work against the result, or that the database contains what you think it does. Only an actual restore tells you that, which is why a periodic restore to a scratch server belongs on the calendar rather than on the wish list.

Scheduling backups, and noticing when they stop

pgBackRest has no scheduler. You supply one, and the usual choice is cron or a systemd timer running as postgres.

A schedule that matches the retention example above is a weekly full and a daily differential:

# /etc/cron.d/pgbackrest
0 2 * * 0 postgres pgbackrest --stanza=billing --type=full backup
0 2 * * 1-6 postgres pgbackrest --stanza=billing --type=diff backup

Add hourly incrementals if your recovery point objective calls for them:

0 * * * * postgres pgbackrest --stanza=billing --type=incr backup

A systemd timer is the better choice on a modern host, because failures land in the journal with an exit status rather than in an email nobody reads, and systemctl list-timers shows you when the next run is due.

That covers taking the backup. It does not cover finding out when one stops happening, and those are genuinely separate problems. Cron reports a failure by sending mail to a local mailbox, which on most servers goes nowhere.

The cheapest thing that works is to alert on backup age rather than on job failure. A job that fails loudly is easy; a job that silently stops being scheduled, or succeeds while writing nothing, is the one that hurts. info reports timestamps in a machine-readable form:

sudo -u postgres pgbackrest --stanza=billing info --output=json

Parse the newest backup's timestamp, compare it against your expected interval, and page if it drifts. Whatever you already run for monitoring can do this. The important part is that something outside the backup host is asking the question, because a check that runs on the machine that died cannot tell you the machine died.

Set the alert threshold from your recovery point objective, not from the schedule. If you take hourly incrementals but can tolerate losing a day, alerting at 90 minutes generates noise nobody acts on, and an alert nobody acts on is worse than no alert because it teaches the team to ignore the channel.

Where pgBackRest stops

pgBackRest is very good at the thing it does. It is worth being clear about the boundary, because a tool doing its job well is often mistaken for a complete backup system.

It does not schedule itself. As the previous section covered, you supply cron or a systemd timer, and from that point your schedule has the same properties as any other cron-driven backup: it runs, and it does not tell you when it stops running.

It does not alert. A failed backup writes to a log file. If nobody reads the log, the failure is silent, and the most expensive backup failures are the silent ones. The age check above is the minimum, and it is work you have to do and maintain.

It does not verify restores. verify checks file integrity, which is not the same as a tested restore.

It covers one engine. If your estate is only PostgreSQL, that is fine. If it also includes MySQL, MongoDB, Redis and a set of servers, pgBackRest solves one part and you build or buy the rest.

It has an operational cost. Repository credentials, cipher passphrases, retention tuning, upgrades, and a runbook someone can follow at 3am. That cost is worth paying on a large self-hosted cluster and is disproportionate on a 20 GB database where a nightly dump to object storage would do.

On that last point, it is worth knowing what the alternatives are for. Barman and WAL-G occupy similar ground with different trade-offs, and our comparison of PostgreSQL backup tools covers where each one fits. If you are choosing between writing a backup script of your own and adopting a tool, the honest question is whether your problem is capability or operations. pgBackRest answers the first. It does not answer the second.

What to do next

If you are starting from nothing, the order that works is: install, write a minimal stanza with a local repository, get check passing, take one full backup, and restore it onto a scratch server the same afternoon. Only then move the repository to object storage and add encryption, retention and a schedule. Teams that configure everything at once and take their first restore months later are the ones who discover a problem at the worst moment.

Then put two things in the calendar rather than the backlog: an alert on backup age, and a restore rehearsal every quarter.

If the scheduling, alerting and restore-verification layer around your backups sounds like a second job, that is the part SimpleBackups handles: backups on a schedule, sent to storage you control, with an alert when a run fails, across PostgreSQL, MySQL, MongoDB, Redis and your servers.

Keep learning

  • Choosing a backup format, because if you keep logical dumps alongside physical backups, -Fc is the flag that decides whether selective restore is available to you later.
  • Backing up Postgres in Docker, if your cluster runs in a container. The data directory lives somewhere different, which changes the pg1-path in every example above.
  • The pgBackRest user guide, which is thorough and version-specific, and the reference to trust over any blog post including this one.

FAQ

Does pgBackRest replace pg_dump?

No, they solve different problems and most production clusters end up running both. pgBackRest takes physical backups of an entire cluster and archives the Write-Ahead Log, which is what gives you point-in-time recovery and fast restores on large databases. pg_dump takes a logical backup of a single database as SQL, which is portable across major versions and across providers. A pgBackRest backup cannot be restored into a different PostgreSQL major version, and a pg_dump file cannot recover you to 14:32 last Tuesday.

What is a stanza in pgBackRest?

A stanza is pgBackRest's unit of configuration: it binds one PostgreSQL cluster to one repository where its backups and WAL are stored. Every pgBackRest command takes a --stanza argument, and you create one with stanza-create before taking the first backup. If you run several clusters, each one gets its own stanza, and a stanza name should describe the cluster rather than the environment so it survives a failover.

Can pgBackRest back up directly to S3?

Yes. Set repo1-type=s3 along with the bucket, endpoint, region and credentials, and pgBackRest writes backups and archived WAL straight to object storage with no intermediate local copy. Azure Blob Storage and Google Cloud Storage are supported the same way, as is any S3-compatible provider by pointing repo1-s3-endpoint at it. You can also configure more than one repository so the same backup lands in two places.

Does pgBackRest support incremental backups?

Yes, and it distinguishes two kinds. A differential backup contains everything changed since the last full backup, so restoring needs the full plus that one differential. An incremental backup contains everything changed since the previous backup of any type, so restoring needs the full plus every incremental in the chain. Recent versions also support block-level incremental backup, which stores only the changed parts of a file rather than the whole file.

Can pgBackRest restore a single database instead of the whole cluster?

Partially, and the caveat matters. The --db-include option restores a selected database while replacing the others with stub files, so the cluster starts but the excluded databases are unusable and must be dropped. This is not the selective restore that pg_restore gives you from a custom-format dump, where you can pull back a single table. If you need table-level recovery, keep logical dumps alongside your physical backups.

Is pgBackRest still actively maintained?

Yes. Version 2.59.0 was released on 20 July 2026 and added support for PostgreSQL 19. The project publishes a release history and a list of corporate sponsors, and it supports ten PostgreSQL versions at a time. If you have seen discussion suggesting otherwise, it concerns changes in who leads the project rather than whether releases are still shipping.


This article is part of The complete guide to PostgreSQL backup, an honest, practical reference from the team that backs up PostgreSQL every day.