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.

a computer screen with a blue background
Listen to this article
--:--

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.

Lines of colorful JavaScript code displayed on a dark screen

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:

JavaScript code displayed on a dark screen with colorful syntax highlighting
  • 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:

bash
sudo apt-get update
sudo apt-get install -y pgbackrest

Create a repo directory (even if your primary repo is S3, pgBackRest still uses local paths for temp and config):

bash
sudo mkdir -p /etc/pgbackrest
sudo mkdir -p /var/log/pgbackrest
sudo chown -R postgres:postgres /var/log/pgbackrest
sudo chmod 750 /var/log/pgbackrest

Sanity checks:

bash
pgbackrest version
psql -c "select version();"

Decide your restore targets first

Before you touch config, write down two numbers:

  1. RPO (recovery point objective). Example: “≤ 15 minutes of data loss.”
  2. 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.

Computer screen displaying code and text

Minimal pgBackRest config

Create /etc/pgbackrest/pgbackrest.conf:

ini
[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=postgres

Adjust pg1-path to your data directory.

Now create the stanza:

bash
sudo -u postgres pgbackrest --stanza=main stanza-create
sudo -u postgres pgbackrest --stanza=main check

If 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:

conf
archive_mode = on
archive_command = 'pgbackrest --stanza=main archive-push %p'
archive_timeout = 60s
wal_level = replica
max_wal_senders = 10

Reload or restart Postgres:

bash
sudo systemctl restart postgresql

Why 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:

bash
sudo -u postgres pgbackrest --stanza=main check

Configure 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:

ini
[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=3

Notes:

  • repo1-path is 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 zst because 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:

bash
sudo -u postgres pgbackrest --stanza=main --type=full backup
sudo -u postgres pgbackrest --stanza=main info

That 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:

  1. Weekly full (Sunday 02:00)
  2. Daily incremental (every day 02:00)
  3. 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:

bash
0 2 * * 0 postgres pgbackrest --stanza=main --type=full backup

Daily incremental:

bash
0 2 * * 1-6 postgres pgbackrest --stanza=main --type=incr backup

If you add a differential (say Wednesday):

bash
0 2 * * 3 postgres pgbackrest --stanza=main --type=diff backup

Concrete 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:

ini
[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=time

What 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 GB2.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)

  1. Pick a target: latest, or PITR target time.
  2. Provision a clean restore host (or a new data directory on the same host).
  3. Pull the backup with pgBackRest restore.
  4. Start Postgres in recovery.
  5. 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.

bash
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17/main/*
sudo -u postgres pgbackrest --stanza=main restore
sudo systemctl start postgresql

Then validate:

bash
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:

bash
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 postgresql

Postgres 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.

  1. Bad S3 credentials
    • Change the IAM policy or rotate keys without updating.
    • Expected: backups fail fast, alert fires within 5 minutes.
  2. Archive command broken
    • Set archive_command to a failing command.
    • Expected: pg_stat_archiver shows failures, alert fires before RPO is exceeded.
  3. 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.
  4. Network egress blocked
    • Block outbound to S3 on the DB host.
    • Expected: backup and archive failures are distinct, alerts point to the right subsystem.
  5. 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.

Continue reading

a bunch of wires that are connected to a server

pgBackRest vs Barman vs WAL-G Compared [2026]

pgBackRest nearly died in April 2026 when its sole sponsor pulled out. Here's what happened, who rescued it, and how it compares to Barman and WAL-G for production PostgreSQL backups.

postgres psql terminal linux server — illustration for article on Transparent Huge Pages + Postgres: Stop

Transparent Huge Pages + Postgres: Stop P99 Latency Cliffs [2026]

THP isn’t “free performance” for Postgres. Here are the exact Linux settings to avoid p99 latency cliffs, plus a validation loop and Kubernetes guardrails.

MongoDB vs PostgreSQL 2026: Which Database Actually Wins?

MongoDB vs PostgreSQL 2026: Which Database Actually Wins?

I'd pick MongoDB for rapidly evolving document-heavy workloads and PostgreSQL for anything that touches relational integrity, analytics, or complex queries. Here's the exact fault line I hit running both in production.

Cite this article
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