I have lost count of how many times a cron job silently failed and I only found out days later. The log rotated away, the error never got reported, and the task I thought was running every hour had been dead since Tuesday. Cron is everywhere, it is ancient, and it will absolutely betray you if you do not respect it.
1. Always Log Output
By default, cron discards stdout and emails stderr. If you do not have a local MTA configured ( and honestly, who does anymore ), that email goes nowhere. The errors vanish. Your job fails silently and you are none the wiser.
Here is the fix. Redirect everything to a log file:
0 */6 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1The >> appends instead of overwriting. The 2>&1 sends stderr into the same stream. Now you can actually see what happened ( or did not happen ).
2. Use Full Paths for Everything
Cron runs with a stripped-down PATH. Usually just /usr/bin:/bin. Your script works fine in your shell because you have /usr/local/bin, ~/.local/bin, and whatever else in your PATH. Cron does not care. It will silently fail to find python3, docker, or literally anything you installed outside the system defaults.
Two options. Set PATH at the top of your crontab:
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * * /usr/local/bin/docker compose -f /opt/app/docker-compose.yml up -dOr just use absolute paths everywhere. I prefer the PATH approach because it keeps the crontab readable, but either works. Pick one and be consistent.
3. Test in the Cron Environment
Running a script from your shell is not the same as running it from cron. Your shell has environment variables, aliases, and a full PATH. Cron has none of that. To test what cron actually sees:
# Run your script exactly as cron would
env -i /bin/bash -c '/opt/scripts/myscript.sh'
# Or simulate cron's environment
crontab -l | grep -v '^#' | while read line; do
eval "$line"
doneThe env -i trick strips your entire environment so you can see what breaks. I have caught more PATH issues this way than I want to admit.
4. Lock Your Jobs
If a cron job takes longer than its interval, the next run starts while the previous one is still going. For database backups, file syncs, or anything that touches shared resources, this is a recipe for corruption.
Use flock. It is simple and it works:
# Only one instance at a time
0 */2 * * * flock -n /tmp/backup.lock /opt/scripts/backup.sh >> /var/log/backup.log 2>&1The -n flag means non-blocking. If the lock is held, the job just skips this run. No overlap, no corruption, no drama. Put the lock file in /tmp so it cleans up on reboot ( or use /var/lock if you want persistence ).
5. Monitor or It Did Not Happen
Logging is step one. Monitoring is step two. You need to know when a job fails, not discover it two weeks later when someone asks why the reports are stale.
The simplest approach is a wrapper script that exits non-zero on failure and hooks into whatever alerting you already use:
#!/bin/bash
# run-and-alert.sh - wrap any cron job with alerting
SCRIPT="$1"
LOG="/var/log/cron-$(basename "$SCRIPT").log"
if "$SCRIPT" >> "$LOG" 2>&1; then
echo "[$(date)] OK: $SCRIPT" >> "$LOG"
else
echo "[$(date)] FAIL: $SCRIPT (exit $?)" >> "$LOG"
# Send alert via your preferred method
curl -s -X POST "https://your-monitor.example.com/alert" \
-d "{\"text\": \"Cron job failed: $SCRIPT\"}" \
-H "Content-Type: application/json"
exit 1
fiThen in your crontab:
0 3 * * * /opt/scripts/run-and-alert.sh /opt/scripts/backup.shIf you want something heavier, Healthchecks.io has a free tier. You ping a URL at the end of each job, and if the ping does not arrive within the expected window, it alerts you. Dead simple and it catches the "job never ran at all" case that log files alone cannot.
Cron Timing Cheatsheet
I always forget the cron time format. Here it is for reference ( and for me next time I Google it ):
# ┌───────────── minute (0-59)
# │ ┌───────────── hour (0-23)
# │ │ ┌───────────── day of month (1-31)
# │ │ │ ┌───────────── month (1-12)
# │ │ │ │ ┌───────────── day of week (0-6, Sun=0)
# │ │ │ │ │
# * * * * * command
# Every 6 hours
0 */6 * * * /opt/scripts/sync.sh
# Daily at 3am
0 3 * * * /opt/scripts/backup.sh
# Every Monday at 9am
0 9 * * 1 /opt/scripts/weekly-report.sh
# First of every month at midnight
0 0 1 * * /opt/scripts/monthly-cleanup.shCron is not going anywhere. It is on every Linux box you will ever touch, and it will keep silently failing until you make it loud. Log everything, use full paths, test in the actual environment, lock your jobs, and monitor. Do those five things and cron stops being a trap and starts being a tool.