Investigate a failed backup job
1 · Prerequisites
Confirm every item is in place before any state change.
- The identity of the job that failed: which client, which schedule, which repository or destination, and which data set it protects. A failure notification that names only a job id sends the responder looking in the wrong place for the first ten minutes.
- Read access to the client host where the backup process actually ran, and to its logs. The scheduler that reported the failure is usually not the machine that has the error message.
- Read access to the repository, bucket or destination the job writes to, plus whatever key or passphrase opens it, taken from escrow rather than from the client being investigated.
- The agreed recovery point objective for this data set, written down as a number of hours or minutes. Without it the age of the newest recovery point is a curiosity rather than a decision.
- The time of the last known-good run, from a source outside the failing job itself: the repository listing, a monitoring series, or a success stamp file. A job that fails while writing its own status file will lie about its own history.
- A list of every other client that writes to the same repository or destination, because a shared cause shows up as several jobs failing and the single-client explanation will fit none of them.
- The name of the service owner for the protected data, and the name of whoever can authorise a change to the backup schedule. Neither role is the responder holding the terminal.
- An incident note open before the first command, holding the time each check was run and what it returned. Half of this procedure is a timeline, and a timeline cannot be reconstructed afterwards.
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Establish the age of the newest recovery point before you look at the failure at all. The failed job is the cause; the exposure is how long this system has been without a new recovery point. Those are different quantities, and only the second one tells the incident owner how much trouble the business is in.
- · Count how many consecutive runs have failed, not just the one that alerted. A single miss and a fortnight of silent misses produce the same notification. The repository listing and the schedule together give the answer; the notification alone never does.
- · Check whether other clients writing the same repository or destination also failed. One client failing is a client problem. Several failing at once is a repository, network, credential or capacity problem, and every minute spent on the client is wasted.
- · Confirm the job is genuinely failing rather than genuinely not running. A disabled timer, a decommissioned host and an expired schedule all present as "no recent backup" and none of them writes an error anywhere. Look at the schedule state before hunting for a message that does not exist.
- · Do not restart the job yet. A speculative rerun overwrites the client log you are about to read, may take a lock that then outlives it, and if the destination is short of space it can consume what remains. Read first.
- · Do not run any repair, prune or forget operation while the cause is unknown. These write. If the failure turns out to be repository damage, a write is the one action that can convert a recoverable situation into an unrecoverable one.
- · Confirm the tool version on the client and note it in the incident record. A version change on the client is a common and easily missed cause, and every message you are about to interpret belongs to a specific version.
- · Confirm that the repository or destination is reachable read-only from the client, using the same credential the job uses. This single check separates credential and destination failures from everything else in about thirty seconds.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Record the age of the newest recovery point, in hours, beside the agreed recovery point objective for this data set. This is the finding the incident is actually about. Everything below explains it; nothing below replaces it. Write both numbers down before the investigation moves on.
- 2List the recovery points from the repository itself rather than from the job history. The measured listing showed each snapshot with its own timestamp, host, tags and paths, so the newest recovery point and its age are read from the data that would be restored, not from a status table that the failing job maintains.
- 3Read the client log, not the scheduler result. A scheduler reports the exit status of the process it launched. When that process is a wrapper script, the exit status is the exit status of its last command, which is frequently a notification, a log rotation or a cleanup that succeeded after the backup failed. A green scheduler line above a failed backup is the commonest way this incident stays invisible for weeks.
- 4Find the first error in the client log, not the last. The last line is usually a consequence: a summary, an abort, a retry giving up. The first error names the actual condition, and it is often several hundred lines above the one quoted in the alert.
- 5Classify the failure by signature before touching anything: credential, capacity, lock, source unavailable, destination unreachable, or repository damage. Each has a distinct fingerprint and a different next action. Choosing an action before the classification is how a lock gets treated as damage, and how a full destination gets retried all night.
- 6For a suspected lock, establish which process holds it and whether that process still exists. The measured tool prints
create exclusive lock for repositorywhen it takes one. A lock left behind by a killed run looks exactly like a lock held by a run still working, and the difference is a live process on a named host, not a guess. - 7For a suspected capacity failure, measure free space at the destination and at the client staging path, and state which one is short. Deleting recovery points to make room is an anti-pattern this course treats as an incident in its own right, and it must never be the first move of a capacity investigation.
- 8For suspected repository damage, run the structural check first and then the check that reads and re-hashes the stored data, and record both exit statuses. The measured structural check reported
no errors were foundat exit 0 on a repository that had already been damaged. Only the data-reading pass found it. A single check that passes is not the answer to this question. - 9Treat the repository as under suspicion the moment a data-reading check reports damage, and stop writing to it. In the measured capture the tool named the damaged pack, said the damaged files must be removed to repair the repository, pointed at its troubleshooting guide first, and then printed two
repaircommands. Both are repair actions rather than read-only ones, and neither belongs in an investigation whose scope is not yet known. - 10Select the recovery point you would restore from if you had to restore right now, and name it. Never "the latest". Name it by repository, identifier and timestamp, and state in plain words what work it does not contain, measured from the incident window rather than from the job schedule.
- 11Decision point — is the system inside its recovery point objective, outside it, or unknowable? Inside: fix the job, log the exposure, no escalation. Outside: escalate now, before the repair, because the exposure is already a business fact and somebody other than the responder has to own it. Unknowable, because the repository is under suspicion and no recovery point has been proven readable: escalate as if outside. Record which of the three, the numbers that decided it, and the time.
- 12Decision point — repair in place, redirect the job to a second destination, or stop. Repair in place is right only when the classification is credential, lock, capacity or source, and nothing has cast doubt on the stored data. Redirect to a second destination when the repository is under suspicion and a second one exists, because a new recovery point is worth more than a diagnosis. Stop and escalate when neither is available.
- 13Apply the fix that the classification indicates, one change at a time, and record each one as you make it. A credential renewal, a lock removal, a capacity addition and a source remount are four different changes, and applying them together makes the eventual write-up guesswork.
- 14Run the job once, deliberately, and read its client log from the first line rather than reading only its exit status. One controlled run tests the classification. A retry loop tests nothing and consumes the destination.
- 15Validate by performing a restore from a recovery point written after the fix, into an isolated target, and comparing it against a known digest. The measured successful restore reported
Summary: Restored 7 files/dirs (60.000 MiB)at exit 0 and every file verified against digests taken before the backup. That pairing is the validation. A green job on its own is not evidence that the data can be recovered. - 16Have the service owner confirm, against a sample they choose, that the restored content is the content they expect. The responder can prove that bytes match a digest. Only the owner can say the data is the right data.
- 17Publish the exposure window as a single number: from the last known-good recovery point to the first verified one after the fix, beside the agreed objective. That comparison is the output of this runbook. It is the only part of the investigation the organisation can act on, and it exists only if somebody writes down both ends.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓The age of the newest recovery point at the start of the investigation is recorded in hours, beside the agreed recovery point objective for this data set, and the comparison between them is stated explicitly. It is read from the repository itself: for a restic repository,
restic -r "$RBDR_REPO" snapshotsexits 0 and prints one row per recovery point above a trailing count line. An empty listing is a finding, not a failed command. - ✓The number of consecutive failed runs is recorded, established from the repository listing and the schedule rather than from the alert that opened the incident.
systemctl show "$RBDR_UNIT" --property=Result,ExecMainStatus,ExecMainExitTimestampexits 0 and its three lines are pasted into the record whole rather than summarised. - ✓Whether other clients sharing the destination also failed is recorded as a yes or a no, with the evidence that settled it.
- ✓The first error in the client log is quoted verbatim in the incident record, with its timestamp, and it is identified as the first rather than the last. Note the exit status of the search that found it:
grepexits 0 when it matched and 1 when it did not, and an exit of 1 is itself a finding — the failure left no message in the file you searched, so you are reading the wrong file or the run never started. - ✓The failure is assigned to exactly one signature — credential, capacity, lock, source unavailable, destination unreachable, or repository damage — with the evidence that assigned it.
- ✓Where repository damage was suspected, both checks have been run and both exit statuses recorded:
restic -r "$RBDR_REPO" check, thenrestic -r "$RBDR_REPO" check --read-data. In the measured capture on an already-damaged repository the first printedno errors were foundand exited 0 while the second exited 1 on the same repository, so a single passing check does not satisfy this line. - ✓The recovery point that would have been used for a restore during the exposure window is named by repository, identifier and timestamp, with a written statement of what it does not contain.
- ✓Each decision point has a recorded outcome, the numbers that decided it, the time, and the name of whoever decided.
- ✓A restore from a post-fix recovery point has completed into an isolated target and been verified file by file:
restic -r "$RBDR_REPO" restore "$RBDR_SNAP" --target "$RBDR_TARGET"prints aSummary: Restoredline and exits 0, andmd5sum -c "$RBDR_BASELINE"printsOKagainst every file and exits 0. Both exit statuses are recorded; the restore's alone does not satisfy this line. - ✓The service owner has confirmed, against a sample they chose, that the restored content is usable in business terms.
- ✓The total exposure window is published as one number and compared against the agreed objective, and the incident record says whether the objective was met.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Remove every monitoring silence, alert suppression and maintenance window created during the investigation, and record the time each was removed. A silence that outlives the incident is the reflex mutation this runbook most expects, and it hides the next occurrence of exactly this failure.
- ↶Re-enable any schedule, timer or job that was paused while the cause was being established, and confirm it is active rather than assuming it. A backup schedule left disabled after an investigation is a second, quieter incident with the same consequence as the first.
- ↶Undo any speculative restart of the backup service, agent or daemon that was made before the log was read, and note that it happened. A restart that is not recorded turns the client log into evidence nobody can date.
- ↶Remove any temporary credential, token or access grant issued to reach the destination during the investigation, and record when it was revoked.
- ↶Remove any lock, staging directory or partial archive created by a controlled test run, and record what was removed. Anything left behind under a plausible name will be mistaken for real state by the next responder.
- ↶Return any configuration changed while probing — a retry count raised to reproduce a timeout, a verbosity setting, a temporarily widened exclusion — to its previous value, and confirm from the file rather than from memory.
6 · Escalation
When the runbook isn't enough, contact:
- · The newest recovery point is older than the agreed recovery point objective: escalate to the incident owner immediately, before attempting the repair. The exposure already exists and the decision to accept it is not the responder's to make.
- · A data-reading check has reported damage in the repository: escalate before any repair, prune or forget runs. Every repair path writes, and the decision to write to a repository holding the only copies belongs to the data owner.
- · Several clients sharing one destination failed in the same window: escalate to whoever owns that destination. A shared cause is not a client incident, and treating it as one delays the fix for every client at once.
- · The failures have been running for longer than the retention period of any independent copy, so no unaffected recovery point remains inside the objective: escalate with the exact gap in hours rather than continuing to repair quietly.
- · The credential the job uses cannot be renewed by the responder — an expired key held elsewhere, a revoked token, a role nobody now owns: escalate to whoever owns that credential. This is an identity incident wearing a backup incident's clothes.
- · A restore from a post-fix recovery point fails or fails verification: stop treating this as a job failure, escalate, and move to the failed-restore procedure. The system is unrecoverable until a restore proves otherwise.
A failed backup job arrives looking like a task: something broke, fix it, close the ticket. It is a measurement request. What the organisation needs is how long this system has been without a new recovery point; the failure is only the reason for the number.
Establish the exposure before the cause
$ restic snapshotsID Time Host Tags Paths Size
-------------------------------------------------------------------------------
3fe43af4 2026-08-28 13:27:02 8211a08b55c3 daily /work/prod 60.000 MiB
3e349a12 2026-08-28 13:27:03 8211a08b55c3 daily /work/prod 60.000 MiB
-------------------------------------------------------------------------------
Timestamps shown in local time
2 snapshotsEach row carries its own timestamp, host, tags and paths. Subtract the newest timestamp from now, and you have the exposure. Count the rows against the schedule, and you have how many runs were missed rather than how many alerted.
RBDR_REPO=/srv/rbdr-repo
RBDR_RPO_HOURS=24
restic -r "$RBDR_REPO" snapshots
RBDR_NEWEST='2026-08-28 13:27:03'
RBDR_AGE_HOURS=$(( ( $(date +%s) - $(date -d "$RBDR_NEWEST" +%s) ) / 3600 ))
printf 'newest recovery point: %s h old, objective %s h\n' "$RBDR_AGE_HOURS" "$RBDR_RPO_HOURS"
Read the client log, not the scheduler result
A scheduler reports the exit status of the process it launched. Almost every production backup runs through a wrapper: quiesce, back up, prune, notify, rotate a log. The wrapper’s exit status is its last command’s. When the notification succeeds after the backup failed, the scheduler records success and the estate goes on believing it is protected.
RBDR_UNIT=rbdr-backup.service
RBDR_LOG=/var/log/rbdr-backup.log
systemctl show "$RBDR_UNIT" --property=Result,ExecMainStatus,ExecMainExitTimestamp
journalctl -u "$RBDR_UNIT" --since '3 days ago' --no-pager
RBDR_HITS=$(grep -nE 'Fatal|error|denied|space|lock|unable' "$RBDR_LOG")
printf 'match search exit: %s\n' "$?"
printf '%s\n' "$RBDR_HITS" | head -n 20
Take the exit status from the search itself rather than from a pipeline, because
the status of grep | head is head’s and head almost always succeeds. grep
exits 1 when nothing matched, and that is a finding: the failure left no message
in the file you searched. grep -n returns matches in file order, so in an
append-ordered log the top of the list is the earliest. Start there. The last
error is usually a consequence — an abort, a summary, a retry giving up — while
the first names the condition.
Signatures
| Signature | What it looks like in the client log | First probe |
|---|---|---|
| Credential | Authentication, permission or token rejection, before any data moves | Read the destination with the job’s own credential |
| Capacity | No space left, quota exceeded, a write that stops at a repeatable byte count | Free space at the destination and at the client staging path |
| Lock held | A wait, a timeout on acquiring a lock, or a refusal naming another run | Whether the holding process still exists, on the named host |
| Source unavailable | A path, mount, snapshot or database that could not be opened or quiesced | Mount state and readability of the source, from the client |
| Destination unreachable | Timeouts, resets, name resolution failures, 5xx from an endpoint | Reachability and endpoint health, with the transfer path unchanged |
| Repository damage | A named stored object failing verification or decryption | The check that reads the stored data, never a write |
The lock case is both common and easy to misread. The measured tool announces
create exclusive lock for repository when it takes one, and a lock abandoned by
a killed run is indistinguishable from one held by a run still working — unless
you go and look for the process.
The signature that hides
Repository damage is the one signature that can pass a check and still be there.
$ restic check--- does plain 'restic check' notice? ---
using temporary cache in /tmp/restic-check-cache-2185980449
create exclusive lock for repository
load indexes
check all packs
check snapshots, trees and blobs
[0:00] 100.00% 2 / 2 snapshots
no errors were found
>>> exit code: 0no errors were found, exit 0, on a repository that was already damaged. The
structural check reads indexes, trees and blob metadata; the pass that
additionally reads and re-hashes every pack found the damaged pack and exited 1
on the same repository moments later. One passing check has not answered this
question.
RBDR_REPO=/srv/rbdr-repo
restic -r "$RBDR_REPO" check
printf 'structural check exit: %s\n' "$?"
restic -r "$RBDR_REPO" check --read-data
printf 'data-reading check exit: %s\n' "$?"
Recovery point selection
Name the recovery point you would restore from if the incident escalated while you were still reading logs. Not “the latest” — the newest one that predates the incident window and is unaffected by whatever the classification found. Write it as repository, identifier and timestamp, then write the sentence that matters: what work is not in it. For an hourly-changing data set, a recovery point from nine days ago is not “a backup we have”, it is eight and a half days of lost work with a name.
Decision points
| Question | Answer | What follows |
|---|---|---|
| Newest recovery point vs objective | Inside | Repair, record the exposure, no escalation |
| Newest recovery point vs objective | Outside | Escalate now, in parallel with the repair |
| Newest recovery point vs objective | Unknowable — repository under suspicion | Escalate as if outside |
| Where to write the next recovery point | Destination trusted | Repair in place, then one controlled run |
| Where to write the next recovery point | Destination under suspicion, second one exists | Redirect first, diagnose after |
| Where to write the next recovery point | Under suspicion, no second destination | Stop and escalate |
The second question gets decided too slowly. When the repository is suspect and a second destination exists, a new recovery point written somewhere trustworthy is worth more to the business than a diagnosis of the old one.
Abort criteria
Stop, and escalate rather than continuing, when any of these is true:
- A data-reading check has reported damage and the next proposed action writes to
that repository. The measured capture printed
restic repair packsandrestic repair snapshots --forget, under a message saying the damaged files must be removed to repair the repository and that the troubleshooting guide should be read first. Both are repairs. Neither is an investigation. - A prune, forget, compaction or retention pass is scheduled to run against the repository during the investigation and has not been stopped.
- The classification changes between runs, or the set of failing objects grows. Something still has write access that nobody has accounted for.
- The exposure has passed the objective and repairs are being attempted without the incident owner knowing.
- The only remaining candidate action is “run it again and see”, for the third time.
Business validation
A green job proves a process ran. It does not prove the data can be recovered, and this course exists because those two are routinely confused. Close the investigation with a restore.
$ restic restore 3fe43af4 --target /work/restore; md5sum -crestoring snapshot 3fe43af4 of [/work/prod] at 2026-08-28 13:27:02.65376235 +0000 UTC by root@8211a08b55c3 to /work/restore
Summary: Restored 7 files/dirs (60.000 MiB) in 0:00
>>> exit code: 0
--- comparing the restored tree against the 09:00 checksums ---
./app/app.conf: OK
./app/orders.csv: OK
./db/data.bin: OK
>>> md5sum -c exit code: 0Both halves are required. The exit status describes a transfer; the digests describe the bytes. Restore into an isolated target, never over the live data, and let the service owner pick the sample they want to see.
RBDR_REPO=/srv/rbdr-repo
RBDR_SNAP=3fe43af4
RBDR_TARGET=/srv/rbdr-postfix-restore
RBDR_BASELINE=/srv/rbdr-baseline.md5
restic -r "$RBDR_REPO" restore "$RBDR_SNAP" --target "$RBDR_TARGET"
cd "$RBDR_TARGET/work/prod" || exit 1
md5sum -c "$RBDR_BASELINE"
What to record
The starting age in hours beside the agreed objective; the number of consecutive missed runs; whether other clients sharing the destination failed; the first client-log error verbatim with its timestamp; the signature and the evidence for it; both check exit statuses where damage was suspected; the recovery point you selected and what it does not contain; each decision point with the numbers that settled it; the restore result beside the owner’s confirmation.
And the number the organisation will use: the exposure window, from the last known-good recovery point to the first verified one after the fix, published beside the objective it was measured against.
Cross-course references
- Linux for Production Sysadmins, Part XXXVI (Scheduled Operations) — timers, wrapper scripts and why a scheduler’s exit status is not the job’s.
- Linux for Production Sysadmins, Part XLVIII (Backup Tools) — the client-side mechanics whose logs this procedure reads.
- Observability for Production Sysadmins, Part CIII (Alert Failure) — why the absence of an alert is not the absence of a failure.
- Ceph & Distributed Storage, Part LXIV (Nearfull, Backfillfull and Full) — the capacity signature seen from the destination side.
- Secrets, PKI & Certificate Management, Part XVI (Rotation Without Outage) — the credential expiry that quietly stops a backup job at 02:00.