Your Postgres data is on fire, and you haven’t tested recovery
You know the feeling. It’s 2 AM. A query just deleted the wrong rows. Your app is screeching to a halt. You reach for that backup you set up three months ago and… nothing. You don’t actually know if it works.
Postgres backups aren’t as simple as “dump the database and call it a day.” Well, they can be. But if you want point-in-time recovery (PITR), 24-hour RPO, automated retention, and cloud-native handling of terabytes of data, you need to pick the right tool.
Here’s the thing: pg_dump is a hammer. pgBackRest is a precision drill. WAL-G is the robot arm. Each solves a different backup problem. Let’s walk through when you’d use each one, how to set them up, and—most importantly—how to know they actually work when you need them.
The Three Tools, Head-to-Head
pg_dump: The Hammer
pg_dump ships with Postgres. It’s the easiest backup tool you’ll ever use. It reads your entire database, converts it to SQL, and writes it to a file. Restore is simple: pipe it back into psql and you’re done.
When to use it:
- Databases under 10 GB
- You need a portable SQL export for migration or archival
- Dev/test environments where RPO = “whenever I ran the dump”
- Once-daily backups are fine
- You don’t care about point-in-time recovery
When NOT to use it:
- Large databases (10+ GB) — it locks tables or slows reads during the dump
- You need sub-hour RPO or PITR
- You can’t afford backup windows
- Terabyte-scale data
The real problem: pg_dump reads the entire database sequentially. On a busy production database, this hammers I/O and CPU. And if the dump is interrupted mid-backup, you lose the entire backup — it’s all-or-nothing.
pgBackRest: The Precision Drill
pgBackRest is the mature, production-grade Postgres backup tool. It creates full backups (WAL + filesystem snapshots) and then streams the Write-Ahead Log (WAL) to a repository. It’s been battle-tested at Postgres conferences for years.
When to use it:
- Databases 10 GB to 1 TB
- You want point-in-time recovery and sub-minute RPO
- You have an external backup repository (S3, Azure Blob, MinIO, NFS)
- You need incremental backups (only backup changed blocks)
- Production databases where backup integrity is non-negotiable
When NOT to use it:
- You don’t have persistent object storage (S3/MinIO)
- You want the simplest possible backup (just use pg_dump)
- Terabyte-scale databases at hyperscale (WAL-G might fit better)
Why it’s good: pgBackRest uses Postgres’s backup API, not pg_dump. It’s resilient—if a backup is interrupted, pgBackRest picks up where it left off. WAL archiving is automatic. Retention policies are declarative. It’s actively maintained and widely used.
WAL-G: The Robot Arm
WAL-G is newer, cloud-native, and stupidly fast at scale. It was originally built by Citus Data and is now maintained primarily by Yandex for massive databases in the cloud. It streams WAL segments and full backups directly to S3 (or S3-compatible storage) with parallelized uploads.
When to use it:
- Databases 1 TB and beyond
- You’re running on cloud infra (AWS, GCP, Azure)
- You want the fastest backup and restore times
- You can tolerate Go binary dependencies and newer tooling
- You need automatic WAL compression and deduplication
When NOT to use it:
- You’re on classic on-prem with NFS-only backup storage
- You want the most battle-tested, widely-used tool (pgBackRest has more production deployments)
- Your team has no Go experience and wants pure SQL tools
Why it’s good: WAL-G parallelizes uploads, compresses WAL, and integrates cleanly with cloud ecosystems. Restore is blazing fast because you’re not reading from slow NFS—you’re pulling from S3 in parallel. New features ship frequently.
Setup: Let’s Actually Build These
pg_dump Setup (5 minutes)
# Full backup to SQL filepg_dump -U postgres -d myapp > myapp_backup.sql
# Compressed backup (saves ~70% space)pg_dump -U postgres -d myapp | gzip > myapp_backup.sql.gz
# With custom format (compressed, faster restore)pg_dump -U postgres -d myapp -Fc > myapp_backup.dump
# Backup all databasespg_dumpall -U postgres | gzip > all_databases.sql.gzThat’s it. You now have a backup. Test restore:
# Restore from SQLpsql -U postgres -d myapp < myapp_backup.sql
# Restore from custom format (much faster for large DBs)pg_restore -U postgres -d myapp myapp_backup.dumpCron it:
0 2 * * * pg_dump -U postgres -d myapp -Fc | gzip > /backups/myapp_$(date +\%Y\%m\%d).dump.gzDone. You have daily backups. No PITR. No streaming WAL. But it works.
pgBackRest Setup (30 minutes)
First, you need a repository—S3, MinIO, or NFS. Let’s use MinIO (self-hosted S3-compatible):
# Install pgBackRestsudo apt-get install pgbackrest
# Create pgbackrest system usersudo useradd -m -s /bin/bash pgbackrest
# Create repository directory (or use S3 bucket)sudo mkdir -p /var/lib/pgbackrestsudo chown pgbackrest:pgbackrest /var/lib/pgbackrestsudo chmod 700 /var/lib/pgbackrestConfig file at /etc/pgbackrest/pgbackrest.conf:
[global]repo1-type=s3repo1-s3-bucket=postgres-backupsrepo1-s3-endpoint=minio.local:9000repo1-s3-region=us-east-1repo1-s3-key=minioadminrepo1-s3-key-secret=minioadminrepo1-s3-uri-style=pathrepo1-cipher-type=aes-256-cbcrepo1-cipher-pass=your-secret-passphrase
[postgres]pg1-path=/var/lib/postgresql/16/mainpg1-user=postgrespg1-port=5432
process-max=4compress-type=bz2retention-full=30Initialize the repo:
sudo -u pgbackrest pgbackrest --stanza=postgres repo-createTest connectivity:
sudo -u pgbackrest pgbackrest --stanza=postgres checkCreate a full backup:
sudo -u pgbackrest pgbackrest --stanza=postgres backup --type=fullWAL archiving is now automatic. pgBackRest watches /var/lib/postgresql/16/main/pg_wal/ and ships segments to your S3/MinIO repo.
Cron it:
# Daily incremental backup (only changed blocks since last full)0 3 * * * sudo -u pgbackrest pgbackrest --stanza=postgres backup --type=diff
# Weekly full backup0 4 * * 0 sudo -u pgbackrest pgbackrest --stanza=postgres backup --type=fullList backups:
sudo -u pgbackrest pgbackrest --stanza=postgres infoWAL-G Setup (20 minutes)
Install WAL-G:
# Download latest binary (check releases on GitHub)wget https://github.com/wal-g/wal-g/releases/download/v3.0.0/wal-g-pg.linux-amd64.tar.gztar xzf wal-g-pg.linux-amd64.tar.gzsudo mv wal-g-pg /usr/local/bin/wal-gsudo chmod +x /usr/local/bin/wal-gSet up S3 credentials in a systemd environment file /etc/default/wal-g:
export PGDATA=/var/lib/postgresql/16/mainexport AWS_ACCESS_KEY_ID=minioadminexport AWS_SECRET_ACCESS_KEY=minioadminexport AWS_S3_FORCE_PATH_STYLE=trueexport WALE_S3_ENDPOINT=https://minio.local:9000export WALG_S3_PREFIX=s3://postgres-backups/wal-gexport WALG_COMPRESSION_METHOD=brotliexport WALG_DELTA_MAX_STEPS=5export WALG_DOWNLOAD_CONCURRENCY=4export WALG_UPLOAD_CONCURRENCY=4Configure Postgres for WAL-G archiving. In postgresql.conf:
wal_level = replicamax_wal_senders = 3max_replication_slots = 1archive_mode = onarchive_command = '. /etc/default/wal-g && wal-g wal-push %p'archive_timeout = 300Restart Postgres:
sudo systemctl restart postgresqlCreate a full backup:
. /etc/default/wal-g && wal-g backup-push /var/lib/postgresql/16/mainWAL segments now stream automatically. Check backups:
. /etc/default/wal-g && wal-g backup-listPoint-in-Time Recovery (PITR)
This is where pgBackRest and WAL-G shine.
Say your table got nuked at 14:47 UTC. You want to recover to 14:45 UTC.
With pgBackRest
# List backupssudo -u pgbackrest pgbackrest --stanza=postgres info
# Stop Postgressudo systemctl stop postgresql
# Restore to specific point in timesudo -u pgbackrest pgbackrest --stanza=postgres restore \ --type=time \ --target='2026-05-09 14:45:00+00' \ --restore-path=/var/lib/postgresql/16/main
# Change ownershipsudo chown postgres:postgres /var/lib/postgresql/16/main -R
# Start Postgressudo systemctl start postgresqlDone. Your database is at 14:45 UTC, with all WAL segments replayed up to that point.
With WAL-G
# Stop Postgressudo systemctl stop postgresql
# Restore from S3 to specific time. /etc/default/wal-g && wal-g backup-fetch /var/lib/postgresql/16/main LATEST
# Restore to point-in-time. /etc/default/wal-g && wal-g wal-fetch /var/lib/postgresql/16/main --target-time='2026-05-09 14:45:00+00'
# Change ownership and permissionssudo chown postgres:postgres /var/lib/postgresql/16/main -Rsudo chmod 700 /var/lib/postgresql/16/main
# Start Postgressudo systemctl start postgresqlSame result, slightly different commands.
Retention Policies (So You Don’t Fill Your Disk)
pgBackRest Retention
In /etc/pgbackrest/pgbackrest.conf:
[global]retention-full=30 # Keep 30 full backupsretention-diff=20 # Keep 20 diff backupsretention-archive=90 # Keep 90 days of WALpgBackRest automatically expires old backups and WAL when you run:
sudo -u pgbackrest pgbackrest --stanza=postgres expire(This is usually cron’d daily.)
WAL-G Retention
WAL-G has less built-in retention logic. You manage it manually:
# List backups older than 30 days. /etc/default/wal-g && wal-g delete before LATEST --confirm
# Delete specific backup. /etc/default/wal-g && wal-g delete <backup-name> --confirmAdd a cron job:
0 5 * * * . /etc/default/wal-g && wal-g delete before $(date -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ) --confirmThe Restore Drill (Do This Today)
Reading about backups is easy. Actually restoring is where everything breaks.
Here’s your drill:
-
Take a backup right now
Terminal window sudo -u pgbackrest pgbackrest --stanza=postgres backup --type=full -
Wait 10 minutes. Let some WAL segments accumulate.
-
Do something intentional (so you can verify recovery)
Terminal window psql -U postgres -d myapp -c "INSERT INTO users (name) VALUES ('test-restore');" -
Recover to 5 minutes ago
Terminal window sudo systemctl stop postgresqlsudo -u pgbackrest pgbackrest --stanza=postgres restore \--type=time \--target='now -5 min' \--restore-path=/var/lib/postgresql/16/mainsudo chown postgres:postgres /var/lib/postgresql/16/main -Rsudo systemctl start postgresql -
Verify the row is gone
Terminal window psql -U postgres -d myapp -c "SELECT COUNT(*) FROM users WHERE name = 'test-restore';"
If that row count is 0, your backup works. Now document the recovery procedure and add it to your runbook.
Decision Tree: Which One Do You Pick?
Use pg_dump if:
- Database < 10 GB
- Dev or non-critical production
- One-off exports or migrations
- You don’t care about PITR
Use pgBackRest if:
- Database 10 GB – 1 TB
- You want production-grade PITR
- You have S3/MinIO/NFS repository
- You want the most battle-tested tool with the biggest community
Use WAL-G if:
- Database > 1 TB
- You’re all-in on cloud (AWS/GCP/Azure)
- You want bleeding-edge performance and features
- Your ops team is comfortable with Go tooling
Use all three if:
- You’re paranoid (reasonable for critical databases)
- pg_dump for exports, pgBackRest for streaming PITR, WAL-G for cloud disaster recovery
The Thing Nobody Tells You
Backups don’t matter. Restores matter. A backup you’ve never tested is just data taking up space. Every backup system on this page will sit untested until the moment you need it—at 2 AM, with your boss watching.
So set it up, test recovery today, and add the procedure to your runbook. Then you can sleep through the night.
Your 2 AM self will thank you.