Backup & DRII · RPO, RTO and Recovery SequencingObjectives
RPO: engineering an acceptable loss window
What you'll learn
- Derive a recovery point objective from a stated schedule and a stated failure model
- State a worst-case loss window instead of the loss observed in one incident
- Explain how continuous archiving narrows the window and what actually bounds it
- Identify the archive signals that reveal a window widening with no configuration change
Prerequisites
Practice
Verified against restic 0.19.1 · BorgBackup 1.4.5 · rclone 1.75.0 · MinIO (S3-compatible object storage) RELEASE.2025-09-07T16-13-09Z · OpenZFS 2.4.1 · LVM2 2.03.31(2) · btrfs-progs 6.17.1 · PostgreSQL 18.6 · pgBackRest 2.59.1 · Kubernetes (k3s) and etcd k3s v1.36.3+k3s1, etcd 3.7.1 · Velero 1.18.2 · Docker Engine 29.7.2 · Proxmox Backup Server (documentation only) 4.0.10-1 · Ubuntu (host baseline) 26.04 LTS · 2026-08-28
Reading an estate produces a table of systems and of the mechanisms currently protecting each one. It does not tell you whether any of those mechanisms is adequate, because adequacy is a comparison and the survey supplies only one side of it. The other side is a pair of numbers the business owns: how much recent work it can afford to recreate, and how long it can afford to be without the system. This part derives both, starting with the one that is quoted constantly and derived almost never.
The window is a subtraction, not a setting
The recovery point objective is the amount of recent work an organisation has agreed it can afford to recreate or lose. Two words in that sentence do most of the work. Agreed makes it a business decision rather than a technical one. Recreate matters because the cost of a lost hour is not always the data itself: orders that still exist in an upstream queue, or on paper, can be re-entered, while sensor readings that were never written anywhere else cannot. An RPO that is expensive to meet is sometimes cheaper to absorb, and that judgement belongs to whoever owns the revenue.
What no part of that definition contains is a product. A vendor can tell you how often its software is capable of taking a copy; it cannot tell you how much data you will lose, because the loss depends on when the failure lands relative to the copies you chose to take. The number is a subtraction between two moments, and you own both of them.
Take the commonest arrangement in production: one full backup nightly at 01:00, nothing continuous, and a failure model of “the primary storage becomes unreadable at an arbitrary time”. A failure at 20:00 leaves the 01:00 copy as the newest thing that exists, so nineteen hours of work is gone. That number is a fact about one incident, and it is the wrong thing to write in a design document, because the next failure will not arrive at 20:00.
Exposure under this architecture is a sawtooth. It drops to roughly zero when a backup completes and climbs linearly until the next one. The honest statement of its RPO is therefore up to 24 hours, worst case at the moment before the next backup runs. Two shortcuts are commonly taken instead, and both mislead. Quoting the nineteen hours describes an incident that already happened. Quoting twelve hours — the mean loss if failures are uniformly distributed — describes an average outage, and nobody gets an average outage. They get the one they get.
“Up to 24 hours” is still generous, because it treats the backup as instantaneous. A file-level job that runs from 01:00 to 01:40 has read different files at different moments, so the recovery point of any given file is whenever the job reached it. The window is the interval plus the duration of the copy, and only a database backup that brackets itself and carries its own log has a single, defined instant it is consistent as of.
The failure model matters as much as the schedule. Twenty-four hours is the answer for loss of the primary copy. For an accidental deletion noticed nine days later, the relevant question is whether any surviving copy predates the mistake at all — and if retention holds seven days, the window for that failure mode is not 24 hours but unattainable. One system, one schedule, two entirely different numbers. An RPO stated without its failure model is not a number, it is a mood.
Measured: 45000 rows in the backup, 50000 after recovery
The capture below was taken on PostgreSQL 18.6 to make the subtraction visible rather than argued. A base backup was taken while a workload ran, business continued afterwards, and then somebody deleted every row.
$ pg_basebackup -D /work/base -X stream -c fast >>> exit code: 0
rows contained in the base backup: 45000
--- business continues after the backup: 5,000 more orders arrive ---
rows now : 50000
checksum of the business data : sum(amount)=825025000
recovery target time : 2026-08-28 13:34:40.077562+00
--- and then somebody runs an unqualified DELETE ---
rows after the mistake : 0
pg_stat_archiver:
archived=6 failed=0 last=000000010000000000000005
WAL segments in the archive : 5
000000010000000000000001
000000010000000000000002
000000010000000000000003
000000010000000000000003.00000028.backup
000000010000000000000004
000000010000000000000005The base backup holds 45000 rows and will hold 45000 rows forever. Everything between that figure and the 50000 rows the business actually had is work that exists nowhere inside the backup file, and the only reason it survives is that the write-ahead log segments carrying it were copied to the archive as they completed. Recovery replays them onto the base backup, and the transcript names each one it fetched:
2026-08-28 13:35:12.707 UTC [631] LOG: restored log file "000000010000000000000003" from archive
2026-08-28 13:35:12.708 UTC [631] LOG: starting point-in-time recovery to 2026-08-28 13:34:40.077562+00
2026-08-28 13:35:12.708 UTC [631] LOG: redo starts at 0/3000028
2026-08-28 13:35:12.713 UTC [631] LOG: restored log file "000000010000000000000004" from archive
2026-08-28 13:35:12.713 UTC [631] LOG: completed backup recovery with redo LSN 0/3000028 and end LSN 0/3000120
2026-08-28 13:35:12.713 UTC [631] LOG: consistent recovery state reached at 0/3000120
2026-08-28 13:35:12.713 UTC [625] LOG: database system is ready to accept read-only connections
2026-08-28 13:35:12.726 UTC [631] LOG: restored log file "000000010000000000000005" from archive
2026-08-28 13:35:12.735 UTC [631] LOG: recovery stopping before commit of transaction 836, time 2026-08-28 13:34:42.096745+00
2026-08-28 13:35:12.735 UTC [631] LOG: redo done at 0/52EBC90 system usage: CPU: user: 0.01 s, system: 0.00 s, elapsed: 0.02 s
2026-08-28 13:35:12.735 UTC [631] LOG: last completed transaction was at log time 2026-08-28 13:34:38.041366+00
The verification afterwards reported rows recovered : 50000 (expected 50000)
and sum(amount) : 825025000 (expected 825025000). Three segments were
pulled from the archive. Without them the restore would have stopped where the
base backup ended, returned the 45000 rows it contains, and reported no error at
all. That is the entire lesson in one comparison: the backup determines the
floor, the log determines the window above it.
Notice also what the achieved recovery point actually is. The target was
2026-08-28 13:34:40.077562+00, but the log records recovery stopping before commit of transaction 836 and last completed transaction was at log time 2026-08-28 13:34:38.041366+00. Recovery lands on the last commit at or before
the target, not on the target itself. A window quoted to the second is quoted
more precisely than the mechanism can deliver.
What each continuous mechanism buys, and what it costs
Continuous archiving is the first step away from the sawtooth. Instead of recovery depending on the last full copy, the server hands every completed log segment to an archive command, and recovery replays whatever the archive holds. The window stops being the backup interval and becomes the time between a commit and its segment landing in the archive. The cost is storage and a retention rule that must cover the whole chain from the base backup forward: losing one segment in the middle truncates recovery at that point no matter how many later segments survive.
A quiet cluster exposes the weakness of segment-sized units. If a segment takes
six hours to fill, six hours of committed work sits on the primary and nowhere
else. The upstream documentation covers this directly: archive_timeout forces
a segment switch after a chosen interval so that unarchived data has a bounded
age, and the price is archiving segments that are only partly used, which costs
archive volume in exchange for a narrower window. That trade is the shape of
every option in this section.
Feeding that same series of segments to a second machine already loaded with the base backup gives what PostgreSQL’s own documentation calls a warm standby: a copy that can be brought up holding a nearly-current database. That does not narrow the window, which is still bounded by what has been shipped, but the copy is already unpacked, so recovery time falls and the price is a second machine with its own operational surface. Below that rung the unit of movement stops being the segment: an architecture that moves records as they are generated narrows the window to however far behind the transport is running, and one that will not acknowledge a commit until a second copy holds it drives the window for committed transactions to zero by construction, charging for it in commit latency and in a coupling that has to be managed deliberately whenever the second copy is unreachable.
Every step down that ladder is paid for in money, in latency, or in the number of components that must keep working for the guarantee to hold, and no rung supplies an RPO by itself: choosing one is a business decision informed by what an hour of recreated work costs. Each rung also copies a mistaken deletion as faithfully and as quickly as it copies good work, so protection against that failure comes from retention rather than from speed.
The archive is a moving part, and silence widens the window
pg_stat_archiver describes the data path rather than the job: archived_count
and failed_count for successful and failed archiving attempts, and
last_archived_wal and last_failed_wal for the files each of those counts
last touched. The useful alerting question is not “did last night’s backup
succeed” — it did, and it will keep doing so — but “is last_archived_wal still
advancing, and has failed_count moved”. If
failures have been accumulating for three days, the recovery point is three
days old regardless of what any document claims, and every backup dashboard in
the building is green, because the nightly job is genuinely running.
This is the mechanism by which an RPO degrades with no change request, no deployment and no alarm. Nobody widened the window; it widened itself when a credential expired or a mount went read-only, and the only signal was a counter nobody was watching.
$ pg_ctl -D /work/nowal start waiting for server to start.... stopped waiting
pg_ctl: could not start server
Examine the log output.
>>> exit code: 1
2026-08-28 13:34:37.879 UTC [132] LOG: creating missing WAL directory "pg_wal/archive_status"
2026-08-28 13:34:37.879 UTC [132] LOG: creating missing WAL directory "pg_wal/summaries"
2026-08-28 13:34:37.879 UTC [132] LOG: invalid checkpoint record
2026-08-28 13:34:37.879 UTC [132] PANIC: could not locate a valid checkpoint record at 0/2F20158
2026-08-28 13:34:37.941 UTC [126] LOG: startup process (PID 132) was terminated by signal 6: Aborted
2026-08-28 13:34:37.941 UTC [126] LOG: terminating any other active server processes
2026-08-28 13:34:37.942 UTC [126] LOG: shutting down due to startup process failure
2026-08-28 13:34:37.943 UTC [126] LOG: database system is shut downThe copy is complete, sized correctly and entirely present. It is also unusable, and the RPO of the arrangement that produced it is not a large number — it is undefined, which is a different and worse thing to discover during an incident.
Writing a number somebody else can check
A window is only engineering if the statement carries the assumptions that produced it. The form that survives review names the system, the failure it is stated against, the mechanism that produces it, the worst case rather than the average, and the evidence.
Applied to the cluster in the capture, that reads: for the orders database,
against loss of the primary’s storage, using a base backup plus continuous
archiving, the recovery point is the end of the last successfully archived
segment; the exercise on 2026-08-28 recovered to a target of
2026-08-28 13:34:40.077562+00 and returned 50000 rows with
sum(amount)=825025000, matching the pre-incident figures recorded
independently. Against an unqualified DELETE, the same architecture gives the
same window only while a copy older than the mistake still exists, so the
answer is bounded by retention rather than by archive latency.
Written that way, the claim is falsifiable. Someone can check whether the archive counters are still advancing, whether retention still covers the chain, and whether anybody has recovered to a point inside the window recently. A bare figure on a slide supports none of those questions, which is precisely why it survives so long.
Production discipline
- Derive the window from the schedule and the failure model, and treat any figure that arrives without both as unverified. The 45000 rows in the base backup were fixed by the schedule; the other 5000 were decided entirely by what the archive held.
- Write the worst case, not the incident you remember. For nightly backups at 01:00 with nothing continuous, the statement is “up to 24 hours, worst case at the moment before the next backup”, never the nineteen hours a particular failure at 20:00 happened to cost.
- Monitor the archive as a data path, not as a job. Alert on the failure
count moving and on the last archived segment failing to advance; the
capture recorded
archived=6 failed=0before recovery, and that reading is what made the outcome predictable. - State a separate window for each failure mode. Loss of storage,
accidental deletion and loss of a site are answered by different copies, and
the run with
pg_walemptied producedPANIC: could not locate a valid checkpoint record at 0/2F20158rather than a merely older recovery point. - Prove the window by recovering to a point inside it. The claim here is
supported because a recovery to
2026-08-28 13:34:40.077562+00returned 50000 rows andsum(amount)=825025000, matching an independently recorded property of the data.
Cross-course references
- PostgreSQL for Production Sysadmins — Part XIII (Backup, Archiving and Point-in-Time Recovery) teaches the archiving and recovery configuration whose output this lesson borrows; here the capture is used only to derive a loss window, while that part covers how to build and operate the archive that produces it.
- Observability for Production Sysadmins — Part XVIII (Alerting Rules) is where the counters named above become alerts: a failure count that must stay flat and a last-archived segment that must keep advancing. Without those rules the archive latency that bounds this lesson’s RPO is simply unmeasured.
- Ceph & Distributed Storage for Production Sysadmins — Part CXX (Multi-Site Concepts) has the identical shape at storage level, where the recovery point at the remote site is whatever last arrived, so replication lag bounds the window there exactly as archive latency bounds it here.
Quiz
Knowledge check · 5 questions
Q1. A system takes one full backup nightly at 01:00 and has no log shipping or replication. Storage is lost at 20:00 and nineteen hours of work is gone. What should the design document record as this architecture RPO?
Q2. A cluster takes a weekly base backup and archives log segments continuously to a remote store. The archive command has been failing for three days and no alert exists for it. The primary storage is lost now. What is the recovery point?
Q3. A recovery point objective can widen substantially with no configuration change, no deployment and no failed backup job.
Q4. Which of the following must accompany an RPO figure before it can be reviewed by somebody else? Select all that apply.
Q5. A team runs a weekly base backup and archives log segments to an NFS export, and states "our RPO is five minutes". Explain why that is not yet a supportable claim and name the signals that would make it one.
Passing score: 75%. Answers are checked in this browser.