Skip to main content
RunBook Academy

← All break/fix scenarios in Backup & DR

advancedbdr-rto-breach~50 min

The restore is running correctly and will finish long after the deadline

Reported symptoms

  • A storage controller failure at 02:04 on 2026-08-27 takes the rbdr-ledger file service down, and the documented recovery is a full restore of /srv/rbdr-ledger from the repository
  • The restore starts at 02:14 and is still running at 08:19, having written 1.01 TiB of a 4.10 TiB recovery point
  • The DR plan publishes a recovery time objective of 90 minutes for rbdr-ledger, and the incident commander has been repeating that figure to the business every hour since 02:30
  • The restore log holds no errors, no retries and no warnings, and the transfer rate has not left the band 47 to 49 MiB/s since the first minute of the data stage
  • Every escalation asks what has broken in the restore, and every check of the restore answers that nothing has
  • Projected over the remaining bytes at the rate actually observed, the restore completes at about 03:07 on 2026-08-28, roughly sixteen times the published objective
  • The business is making stand-down and failover decisions against a deadline that passed at 03:44 on the first morning, and nobody has yet told them the real number

Evidence

  • · Stage timings from rbdr-restore-run: repository open and index load 41 seconds, snapshot selection 3 seconds, and a data stage that has been running for 6 hours 5 minutes without crossing a stage boundary
  • · The size recorded with the recovery point at backup time: 4.10 TiB across 11,204,918 files under /srv/rbdr-ledger
  • · A sustained write rate of 48 MiB/s at the restore target, sampled three times during the incident and matching the write ceiling measured on that target before the restore began
  • · The nightly backup history for rbdr-ledger: 84 minutes mean over the last 90 runs, moving a mean of 96 GiB of changed data per run against a 4.10 TiB dataset
  • · The commit that introduced the DR plan row, whose message records the 90 minutes as rounded up from the nightly job mean and notes that no restore timing was available
  • · The restore assurance record for rbdr-ledger: fourteen consecutive monthly tests, all PASS, each restoring a sampled subset of files and checking their checksums, none recording an elapsed time and none restoring the whole dataset
  • · A measured backup-and-restore capture in which a second backup of an unchanged 400 MiB dataset took .73s while a restore of the same 400 MiB took 1.07s, and whose own text states that the numbers must not be carried to other hardware
Diagnosis and resolutionclick to reveal

Root cause

Two defects. The first produced a wrong number; the second is why nobody ever found out it was wrong. The published objective was derived from the backup window. The DR plan row for rbdr-ledger carries 90 minutes because the nightly job has a mean runtime of 84 minutes and the author rounded up. The two quantities are not related. The nightly job is incremental: it moves a mean of 96 GiB of changed data and spends most of its 84 minutes walking metadata to decide what changed. The restore is not incremental and never can be. It writes every byte of the recovery point, 4.10 TiB, and has no scan phase at all. Forty-three times the bytes moved, through a different bottleneck, in a different direction. A number taken from one cannot describe the other, and the error is structural rather than merely optimistic: the restore does not shrink when the daily delta shrinks, it grows with the dataset. The control that should have caught this is the restore assurance programme, and it measured the wrong property. Fourteen monthly tests recorded PASS for rbdr-ledger. Each one restored a sampled subset of files and verified their checksums, which proves the recovery point is readable and answers the question "can we restore". None of them recorded an elapsed time, and none restored the full dataset. The programme therefore produced fourteen consecutive pieces of evidence about restorability and zero pieces of evidence about duration, while the plan required a duration and the template demanded a number in the cell. With no measurement to put there, the only figure available was the backup runtime, and it was used.

Remediation

Nothing about the running restore should be interrupted. It is proceeding at the rate the target can accept, a restart discards six hours of written data, and no faster path exists for these bytes on this hardware. The work is to correct the number the business is deciding against, and then to re-sequence what is left. Recompute the completion time from the two quantities that determine it, and hand the arithmetic over rather than the conclusion, so the incident commander can see what would have to change for the answer to change: ```bash RBDR_SIZE_MIB=4299161 RBDR_RATE_MIBS=48 secs=$(( RBDR_SIZE_MIB / RBDR_RATE_MIBS )) printf 'full restore at %d MiB/s: %dh %02dm\n' \ "$RBDR_RATE_MIBS" $(( secs / 3600 )) $(( secs % 3600 / 60 )) ``` Confirm the rate independently instead of trusting the tool that reports it, by measuring what actually lands on the target over a five-minute window: ```bash RBDR_TARGET=/srv/rbdr-ledger before=$(du -sb "$RBDR_TARGET" | cut -f1) sleep 300 after=$(du -sb "$RBDR_TARGET" | cut -f1) printf 'sustained %d MiB/s\n' $(( (after - before) / 300 / 1048576 )) ``` Then change what the restore is asked to deliver first. The 90-minute objective was written for a service, not for 4.10 TiB, and the service does not need all of it to open. Restore the current financial period ahead of the archive by running the remaining work as an ordered set of paths, smallest business-critical set first, and bring rbdr-ledger up read-only on that subset while the remainder streams in behind it. That converts a single 25-hour outage into a short degraded window followed by a long backfill, without making any of the bytes move faster.

Verification

The correction is verified by a measurement, not by a revised estimate. Run one full timed restore of rbdr-ledger to isolated infrastructure and record the elapsed time for each stage separately: repository open, index load, retrieval, write, and post-restore validation. The figure that goes into the DR plan is the sum of those stages, taken from that run, with the date, the dataset size and the target hardware recorded beside it. Confirm that the published objective and the measurement agree by arithmetic and not by assertion: the recorded dataset size divided by the recorded sustained rate, plus the fixed stages, must reproduce the published figure. If it does not, the published figure is describing something other than this restore. Re-run that division whenever the dataset crosses a growth threshold. An objective measured at 4.10 TiB is a statement about 4.10 TiB, and it expires quietly as the dataset grows. Finally, confirm that the assurance record now carries an elapsed time on every entry, and that a test with no recorded duration is reported as incomplete rather than as PASS.

Prevention

**Time the restore, because that is the number the incident needs.** Backup duration is not a predictor of restore duration; the two operations move different amounts of data through different bottlenecks. A recovery objective that has never been produced by a stopwatch is a target, not a measurement, and the plan should say which of the two it is holding. **Make the objective a function, not a constant.** Published as dataset size divided by measured sustained rate plus the fixed stages, an objective recomputes itself as the estimate ages, and the growth that invalidates it becomes visible before the incident. **Refuse to publish an objective with no measurement reference.** The plan cell should require a pointer to a timed restore, with a date and a dataset size. An empty pointer is a finding; it is the state that let a backup runtime be borrowed. **Separate the two questions the assurance programme answers.** Can we restore, and how long does it take. A sampled restore with a checksum answers the first and is silent on the second, and silence in a monthly PASS reads as a positive result. **Design recovery order, not just recovery.** When the full dataset cannot meet the objective, decide in advance which subset must, and record that ordering in the plan so it is not invented during the outage.

Reported symptoms

A storage controller fails at 02:04 on 2026-08-27 and takes the array under the rbdr-ledger file service with it. The documented recovery is a full restore of /srv/rbdr-ledger. It starts at 02:14.

At 08:19 it is still running, having written 1.01 TiB of a 4.10 TiB recovery point. The DR plan publishes 90 minutes for this service, and that is the number the incident commander has repeated to the business every hour since 02:30.

Nothing in the restore looks wrong. No errors, no retries, no warnings, and a transfer rate that has not left the band 47 to 49 MiB/s since the first minute. Every escalation asks what has broken; every check answers that nothing has. Projected over the bytes that remain, the restore finishes at about 03:07 the following morning.

Evidence provided

The stage timings say where the hours are going.

Read-only / Safesix hours in one stage, forty-four seconds in all the others
$ rbdr-restore-run --status
stage                     elapsed     state
repository open + index   00:00:41    complete
snapshot selection        00:00:03    complete
data                      06:05:12    running

bytes written           1.01 TiB of 4.10 TiB
files written           2,761,004 of 11,204,918
rate (5 min mean)       48.1 MiB/s
errors / retries        0 / 0

Illustrative output

Size against rate is the whole projection, and it needs no tooling:

RBDR_SIZE_MIB=4299161
RBDR_RATE_MIBS=48
secs=$(( RBDR_SIZE_MIB / RBDR_RATE_MIBS ))
printf 'full restore at %d MiB/s: %dh %02dm\n' \
  "$RBDR_RATE_MIBS" $(( secs / 3600 )) $(( secs % 3600 / 60 ))

Then the origin of the published figure, which is recorded in the repository that holds the plan.

Read-only / Safewhere the 90 minutes came from
$ git log -1 --format=%B -- dr-plan/rbdr-ledger.yml
dr-plan: add recovery objectives for rbdr-ledger

RPO 24h, from the nightly schedule.
RTO 90m, rounded up from the nightly job mean of 84m.
No restore timing available; revisit after the next DR exercise.

Illustrative output

The relationship that commit assumed had already been measured, on a dataset small enough to hold the whole comparison in one capture.

Read-only / Safebackup duration against restore duration, and what the measurement does not show
$ the harness summarising backup and restore seconds for two datasets of the same size
==================================================================
SUMMARY OF PART A
==================================================================
                                 backup    restore
400 MiB in 1 file                 1.49s      1.07s
400 MiB in 8000 files             1.29s      1.36s

READ THESE NUMBERS CAREFULLY, INCLUDING WHAT THEY DO NOT SHOW.

The file-count effect here is modest: restoring 8000 small files took
about a fifth longer than restoring the same bytes as one file. This
test deliberately ran on tmpfs, which removes seek time, queue depth and
network entirely, so it isolates the per-file overhead inside the tool
and nothing else. It does NOT measure what the same shapes would cost on
real storage, and no number here should be carried to other hardware.

--- the asymmetry that IS large, and is the point of the exercise ---
a second backup of 'big' with nothing changed:
backup  : .73s

first backup of 400 MiB : 1.49s
second backup, unchanged: .73s
restore of 400 MiB      : 1.07s

The nightly job reports the middle number. The incident needs the last
one. Backup duration is not a predictor of restore duration, because
backup is incremental and restore never is - and the gap grows with
every day the dataset changes slowly.

Take that capture at exactly its own valuation. It establishes a direction, not a multiplier: on 400 MiB the unchanged backup and the restore differ by less than a factor of two, and it calls its own file-count effect modest. Its text forbids carrying the seconds anywhere else. The sixteen in this incident comes from the site’s own dataset size and its own measured rate.

The assurance record completes the picture: fourteen consecutive monthly restore tests for rbdr-ledger, all PASS, each restoring a sampled subset of files and checking their checksums, none recording an elapsed time.

Work the evidence before reading on

  1. Two stages completed in forty-four seconds and one has run for six hours. Which quantity does each kind of stage scale with?
  2. The nightly job moves 96 GiB and the restore moves 4.10 TiB. Before reading further, work out the ratio and compare it with the ratio of the two runtimes.
  3. The capture shows a .73s backup against a 1.07s restore. What does that entitle you to claim about this incident, and what does it not?
  4. Fourteen monthly tests reported PASS. Write down the question each one answered, then write down the question the plan needed answered.
  5. The obvious alternative is that the restore is degraded — a throttled link, a retry storm, a failing target disk. Which piece of evidence eliminates it?

Root cause

The alternative had to be eliminated first

If the restore were degraded, this would be an incident with a fix in it, and the right move would be to find the throttle. Three observations close that door. The rate has stayed inside 47 to 49 MiB/s since the first minute, which is not what contention or retries look like. The driver reports zero errors and zero retries. And 48 MiB/s is the write ceiling measured on this target before the restore started, so it is already consuming everything the target can accept.

The restore is not slow. It is running at full speed and the deadline was wrong. That is the distinction between an incident and a design defect, and it changes who has to act.

The published objective was derived from the backup window

The plan says 90 minutes because the nightly job averages 84. That job is incremental: it moves 96 GiB of changed data and spends most of its runtime walking metadata to decide what changed. The restore has no scan phase and no increment. It writes 4.10 TiB — forty-three times the bytes — through the target’s write path rather than the source’s read path. Two independent errors sit in one number: the byte counts differ by a factor of forty-three, and the runtimes are dominated by different work.

No restore had ever been timed

The assurance programme is why the wrong number survived fourteen months of review. Its monthly test restores a sample and verifies checksums, which answers whether the recovery point is readable. It records PASS, and PASS reads as coverage.

It never recorded a duration and never restored the whole dataset. So when the plan template demanded a figure for the recovery time cell, no measurement existed to put there. The commit message says as much and promises to revisit. The only number in reach was the backup runtime, and a filled cell stops attracting attention.

Resolution

Do not touch the running restore. It is moving at the rate the hardware allows, no faster path exists for these bytes tonight, and a restart discards six hours.

Correct the number the business is deciding against, and hand over the arithmetic rather than the conclusion so the commander can see what would have to change for the answer to change. Confirm the rate independently rather than trusting the tool that reports it, by measuring what actually lands on the target over five minutes.

Then change what the restore is asked to deliver first. Ninety minutes was written for a service, not for 4.10 TiB, and the service does not need all of it to open. Run the remaining work as an ordered set of paths — the current financial period ahead of the archive — and bring rbdr-ledger up read-only on that subset while the remainder streams in behind it. That turns one 25-hour outage into a short degraded window and a long backfill, without making a single byte move faster.

Verification

Run one full timed restore of rbdr-ledger to isolated infrastructure, recording each stage separately. The figure that goes into the plan is the sum of those stages from that run, with the date, the dataset size and the target hardware beside it.

Then check that the published figure and the measurement agree by arithmetic rather than by assertion: dataset size divided by measured sustained rate, plus the fixed stages, must reproduce the published number. Re-run that division when the dataset crosses a growth threshold, because an objective measured at 4.10 TiB is a statement about 4.10 TiB.

Finally, confirm the assurance record now carries an elapsed time on every entry, and that a test with no recorded duration reports as incomplete rather than PASS.

Prevention

Time the restore, because that is the number the incident needs. Backup and restore move different amounts of data through different bottlenecks. An objective never produced by a stopwatch is a target, and the plan should say which of the two it is holding.

Make the objective a function, not a constant. Expressed as dataset size over measured sustained rate plus the fixed stages, it recomputes as it ages, and the growth that invalidates it becomes visible before the outage does. Require a pointer to the timed restore it cites; an empty pointer is a finding, and it is the state that let a backup runtime be borrowed.

Design recovery order, not just recovery. When the full dataset cannot meet the objective, decide in advance which subset must, and record that ordering in the plan so it is not invented at 08:19 during the outage.