How to know if your backup actually ran
A backup job that fails silently looks exactly like one that works, right up until the day you need it.
Backups are the classic silent failure. The job is scheduled, nobody looks at it, and the first real test is the day something is lost. Verifying a backup is three separate questions, and most setups only answer the first one.
Question 1: did it run at all?
Check the scheduler, not the backup tool:
# cron
journalctl -u cron --since "2 days ago" | grep backup
# systemd timers
systemctl list-timers --all | grep backup
systemctl status backup.service
# Windows
# Task Scheduler → the task → History tab (enable All Tasks History first)
If nothing appears, the job never started, and no amount of inspecting the destination will tell you why. See cron job not running or Windows Task Scheduler task didn't run.
Question 2: did it succeed?
Exit code zero is necessary and not sufficient. Real examples of a backup that "succeeded":
tarover a directory that was empty because a mount had not come up yet. Exit 0, and an archive of nothing.pg_dumppiped intogzip: with a plain pipe the shell reports gzip's status, so a failed dump compresses an error message and exits 0.set -o pipefailexists for this.rsyncinterrupted mid-transfer, leaving a partial tree that looks complete to anything counting files.- A dump written to a filesystem that filled up, truncating the file without an error the script checked.
Write the job so its exit code means something:
#!/bin/bash
set -euo pipefail
pg_dump -Fc mydb > /backups/mydb-$(date +\%F).dump
Note the escaped \% if that line lives in a crontab: cron turns an unescaped % into a newline.
Question 3: is the result plausible?
The cheapest useful check is size, compared with recent runs. A dump that is 4 KB when last week's was 300 MB is a failure that exited zero.
SIZE=$(stat -c%s "$OUT")
if [ "$SIZE" -lt 10000000 ]; then
echo "backup suspiciously small: $SIZE bytes" >&2
exit 1
fi
Better, verify the archive can be read: gzip -t, tar -tzf … >/dev/null, or pg_restore --list on a custom-format dump. It costs seconds and catches truncation.
Best, restore it somewhere periodically. The first restore you perform should not be the one you are doing under pressure at 3am. Quarterly is a reasonable floor; monthly if the data changes shape.
The rotation trap
Retention that deletes by age rather than by count of verified backups will happily delete your last good copy while keeping a week of empty ones. If you prune, prune only after the new backup has passed its checks — and never let the pruning step run when the backup step failed.
Being told the night it does not happen
All of the above still requires someone to look. Make the backup report to something outside the machine instead, so silence becomes the alert:
#!/bin/bash
set -euo pipefail
PING="https://vivere.dev/p/<your-monitor-id>"
trap 'curl -fsS -m 10 -o /dev/null "$PING/fail"' ERR
OUT=/backups/mydb-$(date +%F).dump
pg_dump -Fc mydb > "$OUT"
pg_restore --list "$OUT" > /dev/null # it opens
SIZE=$(stat -c%s "$OUT")
[ "$SIZE" -gt 10000000 ] # and it is plausible
curl -fsS -m 10 --retry 3 -o /dev/null -d "ok, $SIZE bytes" "$PING"
Now three different failures raise an alert. The dump failing hits the ERR trap and alerts immediately. The size check failing does the same. And the case no script can catch — the machine was off, the cron entry was removed, the disk was read-only, the container never started — produces no ping at all, and the missed deadline alerts on its own.
Sending the size as the ping body means the alert history doubles as a record of how the backup has been growing, which is where you notice the week it quietly halved.
Common questions
How can I tell if my backup ran last night?
Check three things, not one: that the job ran at all, that it exited successfully, and that the output is a plausible size compared with previous nights. A job that ran and exited zero can still have written an empty file if the source was unreachable.
Why do backups fail silently?
Most backup jobs are scheduled tasks whose output goes nowhere, and most scheduling systems only report a job they started. A disabled timer, a full disk, an expired credential or a machine that was off produce no output at all, which is indistinguishable from success if nobody is checking.
How often should I test a restore?
Often enough that the first restore you do under pressure is not your first restore. Quarterly is a reasonable floor for anything that matters; monthly if the data changes shape frequently. Verifying that the archive opens and contains what you expect catches most of the problems a size check does not.
Find out without looking
Vivere watches for the ping that does not arrive. Add one line to the job, pick where alerts should land, and you hear about the run that never happened.
Start free Read the quickstart
Ten monitors, a status page, email and webhook alerts. No card.
Last reviewed September 2026.
Related
- Cron job not running: how to find out why — A cron job stopped running and nothing told you. The five usual causes in the order worth checking, how to read the logs, and how to find out next time without looking.
- A dead man's switch for your scripts — What a dead man's switch is, why it catches failures that error-based alerting cannot, how to build one with cron and curl, and the timing rules that stop it crying wolf.
- Windows Task Scheduler task didn't run — A scheduled task did not run on Windows. Turning on history, reading the last run result codes, the stored-password trap, and how to know when a task goes quiet.