How to monitor database backups

Updated November 2, 2026

Backups fail in boring ways. The disk fills up, a database password gets rotated, a server is rebuilt and the crontab doesn’t come along. Nothing complains, and the problem shows up on the day you need to restore. The fix is simple: make the backup report every run, and have something notice when the report doesn’t come.

What can go wrong

A backup script that reports

This Bash script for PostgreSQL covers all five cases. It tells Heartcron when it starts, reports any error with the line it failed on, checks the dump size and pings on success with the size in the body:

#!/usr/bin/env bash
set -euo pipefail

PING=https://ping.heartcron.com/<uuid>
FILE=/var/backups/app-$(date +%F).sql.gz

# "started": a run that never finishes is reported as hung
curl -fsS -m 10 --retry 3 -o /dev/null "$PING/start" || true

# any failing command reports the failure right away
trap 'curl -fsS -m 10 --retry 3 -o /dev/null --data-raw "failed at line $LINENO" "$PING/fail"' ERR

pg_dump "$DATABASE_URL" | gzip > "$FILE"

# a dump under 1 MB means something is wrong; adjust for your database
SIZE=$(stat -c %s "$FILE")
if [ "$SIZE" -lt 1000000 ]; then
  echo "dump is only $SIZE bytes" >&2
  false
fi

aws s3 cp "$FILE" "s3://my-backups/$(basename "$FILE")"

curl -fsS -m 10 --retry 3 -o /dev/null --data-raw "size=$SIZE" "$PING"

A few details matter here:

Run it from cron at a quiet minute rather than exactly at midnight, and keep a log:

17 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Set up the check

  1. Create a check with the cron schedule 17 3 * * * and your server’s time zone.
  2. Set the grace period longer than a normal run. If the backup takes 20 minutes, 45 to 60 minutes is reasonable. Because the script sends /start, a run that started but didn’t finish within the grace period is reported as hung.
  3. Pick where alerts go: Telegram, Slack, Discord, email or a webhook.
  4. Put the check’s URL into PING in the script.

MySQL and MariaDB

Only the dump line changes. --single-transaction takes a consistent snapshot of InnoDB tables without locking them, and --quick streams large tables instead of loading them into memory:

mysqldump --single-transaction --quick app | gzip > "$FILE"

Keep the password in ~/.my.cnf with chmod 600 rather than on the command line, where other users can see it in the process list.

Test the alerts once

Monitor restores too

A successful backup only means a file was written. Once a month, restore the latest dump into a scratch database and run a sanity query, such as counting rows in a table that is never empty. Make that a scheduled job with its own check, and you’ll know the backups actually work.

More guides