Guides / PITR

PostgreSQL point-in-time recovery, two ways

Restore a PostgreSQL database to any moment — with zero-setup minute-level snapshots, or to the exact second with encrypted WAL archiving and one restore command.

Why nightly backups aren't enough

A nightly dump means a bad deploy at 6 PM can cost you a full day of writes. Point-in-time recovery (PITR) shrinks that window — your recovery point objective — from 'last night' to minutes, or to the exact second before the mistake.

Tier 1 — minute-level snapshots, zero setup

Schedule a backup every 5–15 minutes. That sounds expensive, but xbackupman hashes the plaintext of every dump during streaming: when nothing changed since the previous run, it stores a pointer instead of a duplicate artifact. An idle database at a 5-minute cadence costs one object per change, not 288 copies a day. Restoring is a picker: choose the moment, get the newest snapshot at or before it.

Tier 2 — to the exact second with WAL archiving

For PostgreSQL, the xbm server agent can archive every WAL segment, sealed with your organization's key before it leaves the machine. Configure postgres once:

wal_level = replica
archive_mode = on
archive_command = 'xbm pitr wal-push %p'
archive_timeout = 60   # a quiet database still ships a segment every minute

-- then take a weekly base anchor:
xbm pitr base --conn "host=127.0.0.1 user=postgres"

Restoring to a moment

One command materializes everything postgres needs — the newest base anchor before your target, every WAL segment since, a recovery.signal and the exact configuration to add. Recovery replays to your target time and stops right before the transaction you're escaping, then promotes to a new timeline.

xbm pitr restore --at "2026-07-23T15:04:05Z" --into /restore
# ...then start postgres on /restore/data — it replays and promotes.

The details that bite

Two things people learn the hard way: a transaction only reaches the archive when a later segment ships (set archive_timeout so quiet periods stay bounded), and recovery_target_time needs at least one archived commit past your target to prove it got there. xbackupman's tooling bakes both lessons in — the enable instructions set archive_timeout, and restore over-fetches segments so postgres always has what it needs.