cron logging that actually tells you what failed

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.

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




Comments
Reading your article brought up a similar situation I realized a few days ago. I have a site that generates configs and it works as it should. Dot the "i's" and cross the "t's"..
I use AI in my build and one test brought up something, I did not think of. The site was built to tested whether configs worked, not whether they were still current. One of my tools, generates configs for SSL/Let's Encrypt and it passes all test but it offers OCSP stapling. That had been discontinued since 2025. So the stapling lines did nothing and all tests passed (with a warning). I have since updated all my tools and tested them against the latest builds.
What I am saying is, you got to check and revalidate everything. Its good at time to have an extra set of "eyes" checking your work, even if its just AI. keep moving forward.
ProxyForge - free Nginx & Apache config generators, tested on real servers
proxyforge.tech
Good input!
Even if the job exits with code 0, you still have to check that it has checked the correct things.
Meet Nix and Bruce @ https://twobirdsonelesbox.com
My project/stuff page @ https://serveraddict.net