rsync and restic Failures: Exit Codes, Locks and the Restore You Never Tested
Backup tools fail in two ways: loudly (a failed run) and silently (a run that "succeeded" while skipping what mattered). This guide covers the loud ones by exit code, then the discipline that catches the silent ones.
rsync exit codes that matter
| Code | Meaning and response |
|---|---|
| 23 — partial transfer | Some files could not be sent: permission denied, vanished mid-run, or a full destination. The per-file lines above the exit code name them; --ignore-errors is a confession, not a fix |
| 11 — file I/O error | Read errors on source (dying disk!) or write errors on target; check dmesg on both ends before rerunning |
| 255 / code 12 — connection | ssh broke mid-transfer: network blip or sshd timeout; for long runs add --timeout/--contimeout and ServerAliveInterval on the ssh side |
| 24 — vanished files | Source files deleted during the run; expected on live systems, noise unless constant |
Exit 23 with Permission denied lines: run as root, or accept it consciously with --super/--no-perms semantics understood — silently skipping unreadable files is how backups lose exactly the sensitive directories.
The trailing-slash bug and other "wrong data" failures
rsync -a /data backup:/bk/ copies the directory; rsync -a /data/ backup:/bk/ copies its contents. The wrong variant produces a backup that restores to the wrong shape — a success that fails at restore. Standardise one form in your scripts and document it. Related traps: forgetting -a (no recursion/permissions), and --delete pointed at the wrong target — test a new rsync line once with -n before scheduling it.
restic: locks, integrity and repository errors
restic -r /repo snapshots # is the repo readable at all? restic -r /repo check --read-data-subset=5% # real integrity sampling restic unlock # ONLY after confirming no backup is running
- "repository is locked": a previous run died (OOM, Ctrl-C, host crash) leaving a stale lock. Confirm nothing is running on any host sharing the repo, then
restic unlock. Never unlock blindly on a shared repo. - OOM during backup of huge trees: restic scans and packs in memory; limit with
--pack-sizeconsiderations or exclude monster directories; the process dying silently mid-run is the classic "no snapshot since Tuesday". - check failures: run
restic repair indexfirst; data-loss-level corruption means restore what verifies from older snapshots — another argument forcheckin the schedule, not justbackup.
Scheduling failures: the backup that never ran
The most common backup outage is a cron that stopped: a changed passwordless-ssh key, a moved repo mount, or the job waiting forever on a hung NFS source. Detect it from the destination side — a freshness check beats reading cron mail:
# on the backup target or a monitor: find /backups/host1 -maxdepth 1 -mtime -2 | grep . || echo "NO FRESH BACKUP"
Alert on snapshot age per host, and on the job's own runtime doubling (a backup that suddenly takes 10× is reading a broken disk or lost its incremental state).
Restore verification: the habit that makes all of this real
- Weekly: restore a random snapshot to scratch space and
diff -ra sample tree; log the result. - Quarterly: full restore drill of one critical dataset, timed, with the runbook in hand.
- For restic:
restic restore --target /drill <snap> --include /critical/path; time it — your RTO is the measured number, not the hoped one. - After every tool or OS upgrade on the backup host, run one drill before trusting the next scheduled run.
Keys: the part of encrypted backups everyone forgets
A restic repository without its key is noise; a key without discipline is a single-file loss of every backup. Keep at least one recovery copy of the key offline and separate from the repository (a different vault, a sealed-envelope process, a secrets manager with its own access review), and prove the copy works: a quarterly drill that restores using the recovery key, not the one cached on the backup host. restic key list and restic key add let several keys protect one repo — issue a dedicated key to the drill so it never touches production credentials. And keep the threat model split in view: object lock makes backups undeletable, encryption makes them unreadable — you need both properties, and "stolen bucket plus leaked key" is the scenario that tests whether key storage was real.
The four numbers to alert on
| Metric | Why |
|---|---|
| Snapshot/run age per host | detects the cron that died |
| Run duration vs baseline | a 10× run reads a dying disk or lost incremental state |
| Exit code != 0 (and != 24 where expected) | loud failures, caught at run time |
| Repo/bucket canary listing | credential and endpoint drift, day one |