cron logging that actually tells you what failed

mikhomikho AdministratorOG Bash Me Gently
4 min read

A backup script has been running at 3am every night for four months. You know this because you checked /var/log/syslog once, back when you set it up, and saw a line like this:

Nov 05 03:00:01 vps01 CRON[8821]: (root) CMD (/usr/local/bin/backup.sh)

That line only ever says one thing: cron attempted to run the command. It says nothing about whether the command succeeded, failed, timed out, or silently wrote an error to a file descriptor nobody's reading. Four months later, the backup has actually been failing since early September, because the remote storage mount it writes to started requiring a renewed auth token, and nothing told you. The only line cron ever logs is "I tried," never "it worked."

Why the default cron behavior hides this

Cron's actual default is almost the opposite of silent, when a job has no output redirection, cron emails the job's stdout and stderr to the local user, assuming a working mail setup exists. On most low-end VPS boxes it doesn't, there's no mail server configured, no MTA relay set up, so that email goes nowhere, isn't even an error, it just evaporates into an unconfigured sendmail or gets refused outright. The failure is invisible because the one place cron tries to put it has no table to land on.

Step one: stop letting output disappear

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

>> /var/log/backup.log appends stdout to a real file. 2>&1 redirects stderr into the same place stdout is going, so error messages land in the same log instead of vanishing into cron's broken mail path. This alone turns "no idea what happened" into "a growing log file," which is progress, but an ever-growing log with no structure is still just as unreadable as /var/log/syslog once it's a few thousand lines long.

Step two: make the script log its own exit status

The fix isn't in the crontab line, it's in the script. A backup script that only prints what it's doing, and never states clearly whether it finished successfully, forces you to read the whole log to find out. Fix that inside backup.sh itself:

#!/bin/bash
LOGFILE=/var/log/backup.log
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

echo "[$TIMESTAMP] starting backup" >> "$LOGFILE"

if mysqldump -u root -p"$DB_PASS" bigdb | gzip > /backups/bigdb-$(date +%F).sql.gz; then
    echo "[$TIMESTAMP] backup succeeded, size $(du -h /backups/bigdb-$(date +%F).sql.gz | cut -f1)" >> "$LOGFILE"
else
    echo "[$TIMESTAMP] backup FAILED, exit code $?" >> "$LOGFILE"
    exit 1
fi

Now the log actually answers the question instead of just recording that an attempt happened:

[2026-09-03 03:00:01] starting backup
[2026-09-03 03:00:14] backup succeeded, size 1.2G
[2026-09-04 03:00:01] starting backup
[2026-09-04 03:00:09] backup FAILED, exit code 1
[2026-09-05 03:00:01] starting backup
[2026-09-05 03:00:08] backup FAILED, exit code 1

September 4th is the actual day things broke, visible in the log the morning after instead of discovered in November when a restore is needed and there's nothing recent to restore from.
LowEndSpirit - VPS Hosting and tech forum

Step three: stop reading the log by hand every morning

Nobody actually opens a log file daily just to check for the word FAILED, that habit lasts about a week. Make the failure find you instead:

*/10 * * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 || echo "backup.sh failed on $(hostname)" | mail -s "Backup failure" [email protected]

|| only runs the second command if the first one exits non-zero, so on a success night nothing extra happens at all. This does depend on mail actually being configured to send somewhere, which circles back to the exact problem cron's default behavior has, so verify this path works by forcing a failure once (exit 1 temporarily at the top of the script) and confirming the email actually lands, not just that the command didn't error locally.

The actual rule

Cron answers one question by default: did it attempt to run. That's never the question you actually care about. Redirect the output somewhere real, make the script state its own result explicitly instead of trusting silence to mean success, and wire a failure path that reaches you without requiring you to go looking. Three small changes, and a failure that would have sat unnoticed for two months instead shows up the next morning.

Meet Nix and Bruce @ https://twobirdsonelesbox.com
My project/stuff page @ https://serveraddict.net

Sign In or Register to comment.