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

- Canonical: https://www.kunalganglani.com/blog/backup-postgresql-pgbackrest
- Author: Kunal Ganglani
- Published: 2026-08-23 · Updated: 2026-08-23
- Category: Cloud and DevOps · Tags: postgresql, backup, pgbackrest, s3, disaster-recovery

## TL;DR

You’ll set up pgBackRest to back up your PostgreSQL database to Amazon S3, keep only the backups you actually need, and prove you can restore when things go wrong. The big idea is “restore-first”: start from what you need to recover (how much data you can lose, and how fast you need to be back), then configure backups, log shipping, and retention to match. You’ll also run real restore drills, including restoring to a specific point in time, and you’ll intentionally break things (bad credentials, missing logs) to make sure alerts and runbooks work.

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](https://cdn.sanity.io/images/vzekdneq/production/28d52912613a6207b673ac7a7cd127fe67ba6fae-1200x675.webp)

It’s not the only option. I’ve compared the main ones in [pgBackRest vs Barman vs WAL-G](/blog/postgresql-backup-tools-compared). 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](https://www.postgresql.org/docs/current/continuous-archiving.html).

## Install pgBackRest and prerequisites

I’m going to assume:

![JavaScript code displayed on a dark screen with colorful syntax highlighting](https://cdn.sanity.io/images/vzekdneq/production/501d14642ca9092f769609996910d69110f94029-1200x675.webp)

- 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.”
1. **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](/pillars/ai-engineering-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](https://cdn.sanity.io/images/vzekdneq/production/04876a288664f2d8e60efdddffc267d1850fabbb-1200x675.webp)

### 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](https://pgbackrest.org/user-guide.html).

### 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)
1. **Daily incremental** (every day 02:00)
1. 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 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](https://aws.amazon.com/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](/blog/local-llm-break-even-cost-model). 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.
1. **Provision a clean restore host** (or a new data directory on the same host).
1. **Pull the backup** with pgBackRest restore.
1. **Start Postgres in recovery**.
1. **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](https://www.postgresql.org/docs/current/continuous-archiving.html).

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**.
1. **Archive command broken**
  - Set `archive_command` to a failing command.
  - Expected: `pg_stat_archiver` shows failures, alert fires before RPO is exceeded.
1. **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.
1. **Network egress blocked**
  - Block outbound to S3 on the DB host.
  - Expected: backup and archive failures are distinct, alerts point to the right subsystem.
1. **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](/blog/prompt-injection-regression-testing-ci). 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.
