Skip to content
Get started

How to monitor a database backup cron job for missed runs

To confirm a database backup cron job actually completed, don't rely on cron's built-in email — it only mails command output when there is output, and sends nothing if cron itself never runs the job. The reliable approach is heartbeat monitoring: have the backup script ping an external monitor as its final action on success, so a missed ping — whether from a failed backup, a down machine, or a removed crontab entry — alerts you before anyone needs the backup.

Why can't cron's MAILTO tell me the backup was missed?

MAILTO routes whatever the backup job prints to stdout/stderr. A job that never runs produces no output, so there is nothing to mail. The critical failure mode for backups is a job that silently stopped days or weeks ago — and the gap isn't discovered until a restore is needed.

How do I add a heartbeat to a database backup script?

backup.sh
#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="/backups"
TODAY=$(date +%Y%m%d)
FILE="$BACKUP_DIR/db_$TODAY.sql.gz"

# Dump and compress. set -e aborts on any non-zero exit.
pg_dump -Fc "$DATABASE_URL" | gzip > "$FILE"

# Verify the backup file is non-empty (a connection error produces an empty file).
[ -s "$FILE" ] || { echo "backup empty"; exit 1; }

# Delete backups older than 30 days.
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +30 -delete

# Only reached on full success.
curl -fsS -m 10 --retry 3 "https://ping.cronshield.com/<your-check-id>"
crontab -e
# Run daily at 02:00. Output to log file (MAILTO also works if MTA is configured).
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Set the monitor's period to daily and the grace period above the expected backup duration. PING_URL is a placeholder for the endpoint you get when you create a monitor. The CronShield receiver ships in an upcoming release.

How do I confirm recent backup files actually exist?

# Check that a backup file for today exists and is non-empty.
ls -lh /backups/db_$(date +%Y%m%d).sql.gz

# Check the most recent backup age (alert if older than 1 day).
find /backups -name "*.sql.gz" -newer /backups/db_$(date +%Y%m%d -d "1 day ago").sql.gz | wc -l

An external heartbeat confirms the script ran; file-existence checks confirm the output is there. Both together give you defense in depth for backup monitoring.

Add a missed-run alert to this job

The free tier gives you a heartbeat endpoint and an email alert when an expected ping doesn't arrive. Paid tiers add the log-aware diagnosis — the last log line and a likely cause in the alert. The heartbeat receiver ships in an upcoming release; see the plans to learn what each tier adds.

Frequently asked questions

How far back should my backup grace period be?
Set the grace period above the expected backup duration, plus some margin. A database that normally takes 10 minutes to dump should have at least a 30-minute grace period. A slow or large backup might take 2-3x its normal time before failing — a wide grace avoids false alarms.
Should I test a restore as part of backup monitoring?
Yes — a backup file that exists but can't be restored is not a backup. Periodically run pg_restore or mysql in a test environment against the most recent backup file to confirm it is valid. Automate this as a separate cron job with its own heartbeat monitor.