Cron is one of the oldest and most reliable tools for scheduling recurring tasks on Linux systems, yet it trips up even experienced administrators. The minimal environment cron provides, its terse syntax, and silent failure modes create a perfect storm for subtle bugs. Most cron job failures share the same handful of root causes. This guide catalogues the most common mistakes and shows you the correct approach for each.
Mistake 1: Assuming the Full Shell Environment
The most frequent cause of "works manually, fails in cron" problems is environment mismatch. When you run a command from your shell, you inherit a rich environment: PATH includes /usr/local/bin, /usr/bin, your home bin directory, and more. Environment variables like LANG, USER, HOME, and application-specific settings are all loaded from your shell profile.
Cron jobs run in a stripped-down environment. The PATH is typically just /usr/bin:/bin, and most environment variables you expect are missing.
The Problem:
30 2 * * * backup-script.sh
This fails silently because backup-script.sh is not in /usr/bin or /bin, and cron cannot find it.
The Correct Approach:
Use absolute paths for all executables and scripts:
30 2 * * * /home/username/scripts/backup-script.sh
For complex jobs, explicitly set the environment at the top of your crontab:
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
HOME=/home/username
30 2 * * * /home/username/scripts/backup-script.sh
Or source your profile inside the script itself:
#!/bin/bash
source ~/.bashrc
# rest of script
When troubleshooting, add env > /tmp/cron-env.txt as a test job to see exactly what environment cron provides.
Mistake 2: Ignoring Script Permissions and Ownership
Cron respects Unix file permissions. If the script is not executable or owned by the wrong user, the job will fail.
The Problem:
You create a script, add it to your crontab, and nothing happens. The script has mode 644 (read/write for owner, read for others) but no execute bit.
The Correct Approach:
Make scripts executable:
chmod +x /home/username/scripts/backup-script.sh
Verify ownership matches the crontab owner:
ls -l /home/username/scripts/backup-script.sh
If you are editing the user crontab with crontab -e, the job runs as that user. If you are editing /etc/crontab or files in /etc/cron.d/, you must specify the user column explicitly:
30 2 * * * username /home/username/scripts/backup-script.sh
For system-wide jobs that need root privileges, use sudo crontab -e or place them in /etc/cron.d/ with the user set to root.
Mistake 3: Redirecting Output Incorrectly or Not at All
By default, cron emails the output of each job to the user's local mail account. On many modern systems, local mail is not configured, so output vanishes. You lose error messages, making debugging nearly impossible.
The Problem:
30 2 * * * /home/username/scripts/backup-script.sh
Errors are mailed to username@localhost, but you never check local mail.
The Correct Approach:
Explicitly redirect output to a log file:
30 2 * * * /home/username/scripts/backup-script.sh >> /home/username/logs/backup.log 2>&1
This appends stdout and stderr to the log file, preserving error messages and command output.
For jobs that should be silent when successful, redirect to /dev/null but keep errors:
30 2 * * * /home/username/scripts/backup-script.sh > /dev/null 2>> /home/username/logs/backup-errors.log
To use the system logger instead:
30 2 * * * /home/username/scripts/backup-script.sh 2>&1 | logger -t backup-job
Errors will appear in /var/log/syslog or /var/log/messages depending on your distribution.
Mistake 4: Misunderstanding Cron Syntax
Cron's five-field time specification is terse and unforgiving. Mistakes in field order, range syntax, or step values break silently.
The Problem:
Wanting a job every 15 minutes, you write:
* */15 * * * /home/username/scripts/check-status.sh
This runs every minute during the 0th, 15th, 30th, and 45th minutes of each hour—far more often than intended.
The Correct Approach:
Understand the field positions: minute, hour, day-of-month, month, day-of-week.
For every 15 minutes:
*/15 * * * * /home/username/scripts/check-status.sh
For specific times, list them explicitly:
0,15,30,45 * * * * /home/username/scripts/check-status.sh
For ranges and steps:
0-23means every value from 0 through 23*/5means every 5th value (0, 5, 10, ...)1-5in the day-of-week field means Monday through Friday- Both day-of-month and day-of-week are ORed together, not ANDed—if either matches, the job runs
When in doubt, test your cron expression with an online validator or use systemd timers with their more explicit OnCalendar syntax.
Mistake 5: Overlapping Job Execution
If a job takes longer to run than its interval, multiple instances may overlap, causing race conditions, lock contention, or resource exhaustion.
The Problem:
*/5 * * * * /home/username/scripts/slow-report.sh
The script takes 8 minutes to complete. At minute 5, a second instance starts while the first is still running. At minute 10, a third starts. You quickly have resource contention.
The Correct Approach:
Use a lock file to prevent overlapping runs:
#!/bin/bash
LOCKFILE="/var/lock/slow-report.lock"
if [ -e "$LOCKFILE" ]; then
echo "Job already running, exiting."
exit 0
fi
trap "rm -f $LOCKFILE" EXIT
touch "$LOCKFILE"
# actual work here
Or use flock for atomic locking:
*/5 * * * * flock -n /var/lock/slow-report.lock /home/username/scripts/slow-report.sh
The -n flag makes flock exit immediately if the lock is held, preventing queued jobs.
For more complex workflows, consider moving to systemd timers with Persistent=true and OnCalendar expressions that naturally prevent overlap.
Mistake 6: Not Accounting for Timezone and Daylight Saving Time
Cron uses the system timezone. During daylight saving time transitions, jobs scheduled at the transition hour may run twice, skip entirely, or shift unexpectedly.
The Problem:
You schedule a job at 2:00 AM. In spring, the clock jumps from 2:00 AM to 3:00 AM—your job never runs. In fall, 2:00 AM happens twice—your job runs twice.
The Correct Approach:
Avoid scheduling critical jobs at DST transition hours (typically 2:00-3:00 AM in regions observing DST). Schedule them at 1:00 AM or 4:00 AM instead.
For timezone-sensitive jobs, explicitly set TZ in the crontab:
TZ=America/New_York
30 2 * * * /home/username/scripts/backup-script.sh
Or set it inside the script:
#!/bin/bash
export TZ=America/New_York
# rest of script
For UTC-based scheduling that ignores local DST:
TZ=UTC
30 6 * * * /home/username/scripts/backup-script.sh
Systemd timers offer finer control with OnCalendar expressions and explicit timezone handling.
Mistake 7: Editing the Wrong Crontab
Linux has user crontabs, the root crontab, /etc/crontab, and per-directory crontabs in /etc/cron.d/. Editing the wrong one leads to jobs not running or running with incorrect privileges.
The Problem:
You edit your user crontab with crontab -e, but the job needs root access. It runs, fails silently due to permissions, and you waste time debugging the script instead of the privilege level.
The Correct Approach:
Use the correct crontab for the task:
crontab -eedits the current user's crontabsudo crontab -eedits root's crontab/etc/crontabis the system-wide crontab (includes a user column)/etc/cron.d/holds drop-in crontab files (also include a user column)
System-wide crontabs use six-field syntax:
30 2 * * * root /usr/local/bin/system-maintenance.sh
User crontabs use five-field syntax (no user column).
To list a user's crontab:
crontab -l
To list root's crontab:
sudo crontab -l
To list all system cron jobs:
ls -l /etc/cron.d/
cat /etc/crontab
Mistake 8: Using Percent Signs Without Escaping
The percent sign % has special meaning in crontab: it signals the start of stdin for the command. Unescaped percent signs break commands that use them, like date formatting.
The Problem:
30 2 * * * /usr/bin/mysqldump mydb > /backup/mydb-$(date +%Y%m%d).sql
The %Y%m%d is interpreted as stdin, and the job fails.
The Correct Approach:
Escape percent signs with backslashes:
30 2 * * * /usr/bin/mysqldump mydb > /backup/mydb-$(date +\%Y\%m\%d).sql
Or move the command into a script:
#!/bin/bash
DATESTAMP=$(date +%Y%m%d)
/usr/bin/mysqldump mydb > /backup/mydb-$DATESTAMP.sql
Then call the script from cron:
30 2 * * * /home/username/scripts/backup-mysql.sh
Mistake 9: Not Monitoring Job Success or Failure
Cron jobs run silently in the background. Unless you actively check logs or monitor exit codes, failures go unnoticed until something breaks.
The Problem:
Your backup job has been failing for three weeks. You only discover it when you need to restore and have no recent backups.
The Correct Approach:
Log job execution with timestamps:
#!/bin/bash
LOGFILE="/home/username/logs/backup.log"
echo "[$(date)] Starting backup" >> "$LOGFILE"
/usr/bin/rsync -a /data /backup/ >> "$LOGFILE" 2>&1
if [ $? -eq 0 ]; then
echo "[$(date)] Backup completed successfully" >> "$LOGFILE"
else
echo "[$(date)] Backup failed" >> "$LOGFILE"
exit 1
fi
Send alerts on failure:
#!/bin/bash
/home/username/scripts/backup-script.sh
if [ $? -ne 0 ]; then
echo "Backup failed on $(hostname)" | mail -s "Backup Failure" [email protected]
fi
Or use a monitoring service: configure cron to POST to a healthcheck endpoint on success:
30 2 * * * /home/username/scripts/backup-script.sh && curl -fsS --retry 3 https://monitoring-service.example.com/ping/backup-job-id > /dev/null
Services like Cronitor, Healthchecks.io, or UptimeRobot can alert you when expected jobs do not check in.
Mistake 10: Forgetting That Cron Runs Non-Interactively
Cron jobs cannot prompt for input, display GUI elements, or assume a terminal. Scripts that work interactively break when run by cron.
The Problem:
Your script uses sudo without NOPASSWD, or calls a command that expects terminal input. Cron hangs or fails.
The Correct Approach:
Ensure all commands are non-interactive. For sudo, add NOPASSWD entries to /etc/sudoers for specific commands:
username ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp
For commands expecting a TTY, use the -n flag if available, or redirect stdin:
30 2 * * * /home/username/scripts/backup-script.sh < /dev/null
Avoid relying on shell aliases, functions, or interactive prompts. If a command needs confirmation, use flags like -y, --yes, or --force.
Troubleshooting Checklist
When a cron job fails, work through this checklist:
- Check cron is running:
systemctl status cronorsystemctl status cronie - Verify the crontab syntax:
crontab -land inspect the entry - Test the command manually with the same user and minimal environment:
env -i HOME=/home/username PATH=/usr/bin:/bin /bin/bash /home/username/scripts/backup-script.sh - Check the logs:
/var/log/syslog,/var/log/cron, or your custom log file - Verify file permissions and ownership:
ls -lon the script - Check for lock file conflicts if using flock or custom locking
- Confirm the timezone and schedule:
dateandtimedatectl - Look for output redirection issues: ensure logs are writable
- Test the cron schedule in isolation: use an online cron expression tester
- Add a test job that just logs the environment to confirm cron is running at all
Conclusion
Most cron job failures stem from a small set of predictable mistakes: assuming the shell environment, ignoring permissions, misunderstanding syntax, and failing to log or monitor execution. By using absolute paths, redirecting output, testing in a minimal environment, and implementing proper locking and monitoring, you can build reliable scheduled tasks that run without surprises. Cron's simplicity is its strength, but only when you respect its constraints.
