Point-in-Time Recovery
Examples on this page use this demo table:
CREATE TABLE orders (id BIGINT PRIMARY KEY, status TEXT);
By the end of this page you will have restored a ScramDB instance to an exact moment in the past, chosen either by timestamp or by LSN, and confirmed you landed exactly where you meant to.
Point-in-time recovery (PITR) is not a separate tool from restore, it is scramdb restore called with --target-lsn <LSN> or --target-time <TIMESTAMP> instead of replaying every WAL segment available. Same command, same manifest, same WAL archive, one extra flag that tells replay where to stop.
Choosing a targetβ
You have two ways to say where you want to land:
--target-time <RFC3339 timestamp>: use this when you know roughly when things went wrong, for example "restore to five minutes before the badDELETEran." This is the more common case: pull the timestamp from an incident timeline, an application log, or a monitoring alert. Accepts microsecond precision, e.g.2026-08-02T09:27:00Zor2026-08-02T09:27:00.123456Z.--target-lsn <LSN>: use this when you have an exact WAL position, for example from a log line ScramDB itself printed (backup and restore both log LSNs), or from monitoring that tracks replication/WAL position directly.
Exactly one of the two is required. Passing both, or neither, is a CLI error:
specify exactly one of --target-lsn or --target-time, not both
restore requires exactly one of --target-lsn or --target-time
Worked example: restore to just before a bad changeβ
This walks through taking a backup, making more changes, then restoring to the moment right before a change you want to undo.
-
Take a backup:
scramdb backup --config /etc/scramdb/config.toml --out file:///var/lib/scramdb/backups/pitr-demoExpect:
backup complete: N file(s), base_lsn=<LSN>, base_wal_file_id=<ID>, destination='file:///var/lib/scramdb/backups/pitr-demo' -
Make more changes, including one you will want to recover from. For example:
INSERT INTO orders (id, status) VALUES (101, 'pending');-- record the time here, e.g. 2026-08-02T09:30:00ZDELETE FROM orders WHERE id = 101; -- the mistake -
Note the timestamp right before the mistake. In this example,
2026-08-02T09:30:00Z, captured right after the insert and before the delete. -
Restore to that timestamp, into a fresh directory:
scramdb restore --config /etc/scramdb/restore-config.toml \--backup file:///var/lib/scramdb/backups/pitr-demo \--target-time 2026-08-02T09:30:00ZExpect:
restore complete: replayed to lsn=<LSN>, target config=/etc/scramdb/restore-config.toml -
Start the restored instance and verify:
scramdb --config /etc/scramdb/restore-config.tomlpsql "host=127.0.0.1 port=5432 user=scramdb dbname=scramdb" -c "SELECT * FROM orders WHERE id = 101;"Expect the row to be present, with
status = 'pending', since the insert happened before your target time and the delete happened after it. If you had instead picked a target time after the delete, the row would be absent. Checking both a row that existed at your target moment and confirming a later change is absent is the strongest verification you can do: it proves you landed on the correct side of the boundary, not just somewhere in the neighborhood.
Verifying you landed where you meantβ
The restore command's own output is your first checkpoint: the lsn=<LSN> it logs is the exact WAL position replay stopped at. Cross-check that against what you expected (for example, a WAL position your monitoring reported near the target time).
Beyond that, query for state you know should differ on either side of your target: a row that existed before but not after, a column value that changed at a known time, a count that should match a known figure as of that moment. A restore that "starts cleanly" but has the wrong data at the boundary is a failed PITR, so verify against the boundary itself, not just server health.
What governs how far back you can goβ
PITR range is bounded by what WAL is actually retrievable, it is not unlimited in either direction:
- You cannot target a point before your backup's own base LSN. Replay only moves forward from the backup; a target before the backup was taken needs an older backup instead.
- How far forward you can go depends on retained WAL. Two settings control this, under
[storage.wal.retention]:min_kept(default1): always keep at least this many closed WAL files locally, regardless of anything else.archive_aware(defaulttrue): once a WAL archiver has started, a local WAL file is not eligible for pruning until the archiver's own watermark has passed it. Do not set this tofalseif you rely on PITR via the archive, doing so lets local WAL get pruned ahead of what the archive has actually captured.
- Whether archiving is enabled at all determines whether you have durable WAL history beyond what happens to still be sitting in the local
wal/directory.
Configuring retention and archivingβ
[storage.wal.archive]
enabled = true
# destination unset -> derives to "{data_dir}/wal-archive" (a local subdirectory of the data volume).
# destination = "file:///var/lib/scramdb/wal-archive"
# poll_interval = "5s"
[storage.wal.retention]
min_kept = 1
archive_aware = true
The shipped Docker image sets [storage.wal.archive].enabled = true by default, specifically so PITR and branching work without extra setup. If destination is left unset while archiving is enabled, WAL archives to {data_dir}/wal-archive, inside the same data volume as everything else; point it elsewhere with an explicit destination (same URL forms as backup: local path, file://, s3://, gs://, az://) if you want the archive on separate storage.
There is no environment-variable form of any of these keys, they are TOML-only, set under [storage.wal.*] in your config file.