How to Back Up PostgreSQL With pgBackRest [S3 + Restore Test]
A restore-first pgBackRest setup for PostgreSQL: S3 repo config, retention math (weekly full + daily incrementals), PITR restore drills, and failure-injection tests.
By the end of this, you’ll have pgBackRest taking PostgreSQL backups to S3, enforcing a weekly full + daily incremental retention policy, and you’ll have run a real restore drill (including PITR) with a failure-injection checklist.
If you follow it cleanly, this is a 60–90 minute setup on a single Postgres host. The restore drills are another 30 minutes.
And yes, this is specifically about how to back up PostgreSQL with pgBackRest in a way that survives the two things that actually kill companies: operator mistakes and “we had backups… somewhere.”
I take a restore-first stance because I’ve watched too many teams treat backups like logging. Something you “turn on” and then forget.
Also: I’m going to intentionally break parts of the system at the end. If that makes you uncomfortable, good. That’s the point.
What is pgBackRest
pgBackRest is an open-source backup and restore tool for PostgreSQL that performs full, differential, and incremental backups, manages WAL archiving for point-in-time recovery, and supports local or object-storage repositories like S3.

It’s not the only option. I’ve compared the main ones in pgBackRest vs Barman vs WAL-G. But in production, pgBackRest tends to win when you care about repeatable restores and operational guardrails.
If you’re new to the mental model, the key idea matches what the PostgreSQL docs describe for continuous archiving and PITR: you restore a base backup and then replay WAL to get to a point in time. That’s straight from the official PostgreSQL documentation.
Install pgBackRest and prerequisites
I’m going to assume:

- You have a running Postgres cluster on Linux.
- You can restart Postgres.
- You have AWS credentials that can write to a dedicated S3 bucket.
Packages, OS user, and directories
Install pgBackRest using your distro packages when you can. On Ubuntu/Debian:
sudo apt-get update
sudo apt-get install -y pgbackrestCreate a repo directory (even if your primary repo is S3, pgBackRest still uses local paths for temp and config):
sudo mkdir -p /etc/pgbackrest
sudo mkdir -p /var/log/pgbackrest
sudo chown -R postgres:postgres /var/log/pgbackrest
sudo chmod 750 /var/log/pgbackrestSanity checks:
pgbackrest version
psql -c "select version();"Decide your restore targets first
Before you touch config, write down two numbers:
- RPO (recovery point objective). Example: “≤ 15 minutes of data loss.”
- RTO (recovery time objective). Example: “restore to service in ≤ 60 minutes.”
These decide everything:
- If your RPO is 15 minutes, your WAL archiving better be healthy and your alerts better fire before 15 minutes.
- If your RTO is 60 minutes, your restores need to be rehearsed, and your backup repo needs enough throughput to pull data back fast.
If you want a production-oriented runbook style, this is the same checklist mindset I use for security drills too. The structure is similar to how I think about AI in production: don’t ship capabilities you can’t verify under stress.
Create and configure a stanza
A stanza is pgBackRest’s unit of configuration for a specific Postgres cluster.

Minimal pgBackRest config
Create /etc/pgbackrest/pgbackrest.conf:
[global]
log-level-console=info
log-level-file=detail
log-path=/var/log/pgbackrest
# Hardening defaults
start-fast=y
process-max=4
[main]
pg1-path=/var/lib/postgresql/17/main
pg1-user=postgresAdjust pg1-path to your data directory.
Now create the stanza:
sudo -u postgres pgbackrest --stanza=main stanza-create
sudo -u postgres pgbackrest --stanza=main checkIf check fails, fix it now. Do not continue. In my experience building this site’s 7-agent publishing pipeline, the boring lesson is: deterministic gates catch the breakage early. Backups are the same. If you let the system limp forward here, you will pay for it later during the restore.
(That “deterministic gates” lesson is from operating my own pipeline on this site: 261+ posts shipped with a hard quality gate and an incident log. It’s not glamorous, but it works.)
Enable WAL archiving (non-negotiable)
Add to postgresql.conf:
archive_mode = on
archive_command = 'pgbackrest --stanza=main archive-push %p'
archive_timeout = 60s
wal_level = replica
max_wal_senders = 10Reload or restart Postgres:
sudo systemctl restart postgresqlWhy archive_timeout = 60s? Because I’d rather ship up smaller WAL segments frequently than discover in an incident that I had a 30+ minute silent RPO hole because traffic was low.
You can validate archiving with:
sudo -u postgres pgbackrest --stanza=main checkConfigure S3 repository (repo1-s3)
S3 is the obvious default repo for most teams because it’s durable, cheap-ish, and doesn’t require you to run another box. But don’t pretend S3 is magic. Credential issues and bucket policy mistakes are one of the most common self-inflicted outages I see.
pgBackRest supports S3-compatible repos. The authoritative details are in the official pgBackRest User Guide.
S3 config block
Extend /etc/pgbackrest/pgbackrest.conf:
[global]
repo1-type=s3
repo1-path=/pgbackrest
repo1-s3-bucket=my-pgbackrest-prod
repo1-s3-region=us-east-1
repo1-s3-key=AKIA...REDACTED
repo1-s3-key-secret=...REDACTED
repo1-s3-uri-style=path
repo1-storage-verify-tls=y
# Compression matters for cost
compress-type=zst
compress-level=3Notes:
repo1-pathis the prefix inside the bucket.- Use a dedicated bucket per environment. Mixing prod + staging backups is how you end up restoring the wrong thing at 3am.
- I’m using
zstbecause Zstandard is a good trade between CPU and size for most workloads.
If you’re on AWS, prefer IAM roles (instance profile / IRSA) over static keys, but pgBackRest config still needs a way to authenticate.
First backup (full)
Take an initial full:
sudo -u postgres pgbackrest --stanza=main --type=full backup
sudo -u postgres pgbackrest --stanza=main infoThat info output is your first reality check. If it doesn’t list your backup and WAL archive status, you don’t have a backup system. You have a wish.
Run full/differential/incremental backups
Here’s the cadence I recommend for most production Postgres clusters that aren’t doing multi-TB ingest:
- Weekly full (Sunday 02:00)
- Daily incremental (every day 02:00)
- Optional: mid-week differential if restores are too slow
The point is to manage restore time. Incrementals are cheap to store and run, but too-long chains are painful to restore.
Cron examples
Weekly full:
0 2 * * 0 postgres pgbackrest --stanza=main --type=full backupDaily incremental:
0 2 * * 1-6 postgres pgbackrest --stanza=main --type=incr backupIf you add a differential (say Wednesday):
0 2 * * 3 postgres pgbackrest --stanza=main --type=diff backupConcrete restore-time example: if your base dataset is 500 GB, pulling a full from S3 at an effective 200 MB/s is ~43 minutes just to read bytes (500 GB / 0.2 GB/s). That’s before Postgres replay and startup. If your RTO is 60 minutes, you don’t get to be casual about chain length.
Configure retention (full/diff) + do the math
Retention is where most teams do “cargo cult” config. They pick a number, feel good, move on.
Don’t.
Pick retention based on:
- Your compliance requirement (e.g. 30 days).
- Your operational reality (how often you discover corruption or operator error).
- Your budget.
A good default: weekly full + 30 days of incrementals
If you take one full per week and daily incrementals, a reasonable starting point:
[global]
repo1-retention-full=5
repo1-retention-full-type=count
# Optional: keep diffs if you take them
repo1-retention-diff=14
repo1-retention-diff-type=timeWhat does 5 fulls mean? 5 weeks of base backups.
Now do the cost math.
S3 cost estimate (simple model)
You don’t need a FinOps team to estimate this. You need a back-of-the-napkin model that’s directionally correct.
Assume:
- Full backup size after compression: 300 GB
- Incremental per day: 10 GB (depends heavily on churn)
- WAL volume per day: 25 GB (busy OLTP systems can be higher)
- Keep 5 fulls (≈ 35 days)
Stored data ≈
- Fulls: 5 × 300 GB = 1500 GB
- Incrementals for 35 days: 35 × 10 GB = 350 GB
- WAL for 35 days: 35 × 25 GB = 875 GB
Total ≈ 2725 GB ≈ 2.7 TB
If you assume S3 Standard storage around $0.023/GB-month (check current region pricing on the official AWS S3 pricing page), then storage cost ≈
2,725 GB × $0.023 ≈ $62.68/month
This is why I push people to stop arguing about whether backups are “expensive.” For most Postgres fleets, the real cost is engineering time. The S3 line item is usually rounding error until you’re at multi-TB scale.
If you want to get fancy, add request costs, lifecycle transitions, and egress for restore drills. But start here.
And if you want an example of cost math discipline applied elsewhere: I did the same kind of break-even modelling for compute in local LLM break-even math. Same habit. Same payoff.
Restore procedure and validation (including PITR)
This is the part everyone skips, and it’s the only part that matters.
Restore checklist (what I actually do)
- Pick a target: latest, or PITR target time.
- Provision a clean restore host (or a new data directory on the same host).
- Pull the backup with pgBackRest restore.
- Start Postgres in recovery.
- Validate data with a few deterministic queries and application-level checks.
Fast restore to latest
Stop Postgres and clear the data directory you’re restoring into.
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17/main/*
sudo -u postgres pgbackrest --stanza=main restore
sudo systemctl start postgresqlThen validate:
psql -c "select now();"
psql -c "select count(*) from important_table;"Make those validation queries part of your runbook. Don’t improvise during an incident.
PITR restore example
Pick a target timestamp like 2026-08-23 14:10:00-04 (Toronto time). Then:
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17/main/*
sudo -u postgres pgbackrest --stanza=main --type=time --target="2026-08-23 14:10:00-04" restore
sudo systemctl start postgresqlPostgres PITR fundamentals are documented in the official manual (again, worth reading): PostgreSQL continuous archiving and PITR.
Validation is the same idea. I also like to validate timeline behavior by forcing a new timeline after recovery (you’ll see it in pg_wal).
Monitoring, alerting, and failure injection tests
If you don’t monitor backups, you’re doing theater.
Monitoring basics
At minimum, alert on:
- No successful backup in X hours (where X < 24 for daily jobs)
- WAL archiving delay (time since last archived WAL)
- pgBackRest check failure
pgBackRest has a “Monitoring” section in its docs, but your real goal is simple: build one signal that answers, “Can I restore to within my RPO right now?”
This is also where I steal habits from my day job building automated systems and from running this blog’s agent pipeline. When I rewrote slugs on live URLs once, I burned 907K impressions of link equity in a single incident. That was a painful reminder that small operational changes can have outsized blast radius. Your backup system is the same. One bucket policy edit can ruin you.
Failure injection test plan (do this quarterly)
Run these in a staging clone, or in a dedicated restore environment.
- Bad S3 credentials
- Change the IAM policy or rotate keys without updating.
- Expected: backups fail fast, alert fires within 5 minutes.
- Archive command broken
- Set
archive_commandto a failing command. - Expected:
pg_stat_archivershows failures, alert fires before RPO is exceeded.
- Set
- WAL gap / missing archive
- Delete a WAL segment in the repo (in a test bucket).
- Expected: PITR restore fails loudly. Your runbook should say what to do next.
- Network egress blocked
- Block outbound to S3 on the DB host.
- Expected: backup and archive failures are distinct, alerts point to the right subsystem.
- Repo corruption simulation
- Flip bits in a test repo object (or remove objects).
- Expected:
pgbackrest check/ restore fails. Your policy should include multi-repo or cross-account copies if you care about this class of failure.
If you want a mindset parallel: this is the same reason I do regression testing for security issues like prompt injection. You don’t “promise” safety. You validate it with adversarial tests.
The leadership-friendly sentence
If you need to justify the work to leadership:
- “We are not buying backups. We are buying recoverability.”
- “We run a restore drill every 90 days, and we can prove RPO ≤ 15 minutes and RTO ≤ 60 minutes on the current dataset.”
That’s how you make backup policy real.
If you’re building this right, the next step is to treat restores like fire drills: schedule them, automate the validation queries, and keep a log of results. My prediction: within 12 months, “show me the last successful restore” will be a standard audit question for more teams than “show me your backup job config.”
Photo by Xavier Cee on Unsplash.
Kunal Ganglani (2026, August 23). How to Back Up PostgreSQL With pgBackRest [S3 + Restore Test]. Kunal Ganglani. Retrieved August 23, 2026, from https://www.kunalganglani.com/blog/backup-postgresql-pgbackrest


