Cron jobs fail silently more often than any other scheduled task system. No popup, no error on screen — the job just does not run and nobody notices until something important is missing. The failures almost always come down to a small set of root causes: PATH issues, environment differences, permission problems, syntax errors, or the cron daemon not running.
This page gives you a systematic diagnostic workflow and covers every common failure pattern so you can fix cron problems quickly and confidently.
systemctl status crondcrontab -l or check /etc/cron.d/journalctl -u crond or /var/log/cron### RHEL / Rocky / AlmaLinux ### # Check crond daemon logs via journalctl sudo journalctl -u crond sudo journalctl -u crond --since today sudo journalctl -u crond -n 50 sudo journalctl -u crond -f # follow live # Traditional log file (if rsyslog is writing it) sudo tail -50 /var/log/cron sudo grep "$(date +%b\ %e)" /var/log/cron # today's entries ### Debian / Ubuntu ### sudo grep CRON /var/log/syslog | tail -30 sudo grep CRON /var/log/syslog | grep "$(date +%b\ %e)"Typical /var/log/cron entries:
The most reliable way to reproduce cron failures — simulate cron's exact environment before the job runs:
# Method 1: Run as the cron user with a minimal environment sudo -u root env -i HOME=/root \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ SHELL=/bin/sh \ /bin/sh /usr/local/bin/myscript.sh # Method 2: Schedule a 1-minute test job that dumps its environment * * * * * env > /tmp/cron-env.txt 2>&1 # Wait 1 minute, then: cat /tmp/cron-env.txt # Method 3: Wrap the script with explicit environment * * * * * /bin/bash -c 'source /etc/profile; /usr/local/bin/myscript.sh' >> /tmp/test.log 2>&1 # Check what environment your interactive shell has vs cron echo $PATH # your PATH cat /tmp/cron-env.txt | grep PATH # cron's PATH
PATH=/usr/bin:/bin and SHELL=/bin/sh.
Your interactive shell has a much richer environment.
Test in cron's environment before concluding the script is broken.
# Symptom in cron log: # CROND: (root) CMD (/usr/local/bin/backup.sh) # CROND: (root) CMDOUT (Permission denied) # Check permissions ls -la /usr/local/bin/backup.sh # Fix -- make executable sudo chmod +x /usr/local/bin/backup.sh # Also check the script's shebang line head -1 /usr/local/bin/backup.sh # Must be: #!/bin/bash or #!/bin/sh etc. # Missing shebang = executed by /bin/sh which may not understand bash syntax # Check scripts in cron.daily/weekly are also executable ls -la /etc/cron.daily/ sudo chmod +x /etc/cron.daily/mybackup
.sh).
run-parts — which executes these directories —
skips files with extensions by default on many systems.
# Symptom: script runs fine manually, silently does nothing in cron # Root cause: command not in cron's minimal PATH # Find where a command actually lives which python3 which node which aws which myapp # Fix 1: Use full paths in crontab # WRONG: 0 2 * * * backup.sh # RIGHT: 0 2 * * * /usr/local/bin/backup.sh # Fix 2: Set PATH at top of crontab PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/myapp/bin 0 2 * * * backup.sh # Fix 3: Use full paths inside the script # Change: python3 script.py # To: /usr/bin/python3 /usr/local/bin/script.py # Fix 4: Source profile inside the script # #!/bin/bash # source /etc/profile # source ~/.bash_profile 2>/dev/null || true
/usr/bin/python3 not
python3. /usr/local/bin/backup.sh not
backup.sh. This eliminates PATH issues entirely.
# In crontab, % is special -- it means newline # This BREAKS silently: 0 2 * * * /usr/bin/find /tmp -name "*.log" -mtime +7 -exec rm {} \; # The backslash before ; is fine, but % in date format breaks: 0 2 * * * echo $(date +%Y-%m-%d) > /var/log/date.txt # BROKEN # Fix: escape % with backslash 0 2 * * * echo $(date +\%Y-\%m-\%d) > /var/log/date.txt # OK # Better fix: put the date command in the script, not the crontab 0 2 * * * /usr/local/bin/log-date.sh # Also watch out for: quotes, $(), and other shell expansion # When in doubt, put complex commands in a script
% is treated as a
newline. date +%Y-%m-%d in a crontab becomes
date + followed by a newline and the rest as stdin.
Always escape with \% or move complex commands
into a script.
# Cron sets HOME but working directory is typically / # Relative paths in scripts fail unpredictably # WRONG -- relative path assumes current directory 0 2 * * * ./backup.sh # WRONG -- script tries to open config.txt from / # #!/bin/bash # cat config.txt # fails -- not in / # FIX -- cd to script directory at start of script # #!/bin/bash # cd "$(dirname "$0")" || exit 1 # cat config.txt # now works -- in same dir as script # Or set explicit working directory in crontab 0 2 * * * cd /usr/local/myapp && ./run.sh # Check HOME and LOGNAME in cron * * * * * echo "HOME=$HOME PWD=$PWD USER=$USER" >> /tmp/cron-dirs.txt
cd "$(dirname "$0")" || exit 1
as the second line of every cron script. It changes to the script's
own directory before doing anything, making relative paths
work correctly regardless of where cron starts.
A cron job with no logging is a black box. Add logging to every job you care about:
# Redirect both stdout and stderr to a log file 0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 # Add timestamps to log output 0 2 * * * echo "=== $(date) ===" >> /var/log/backup.log; \ /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 # Log to syslog using logger 0 2 * * * /usr/local/bin/backup.sh 2>&1 | /usr/bin/logger -t backup-cron # Rotate log files with logrotate -- /etc/logrotate.d/backup-cron sudo tee /etc/logrotate.d/backup-cron << 'EOF' /var/log/backup.log { daily rotate 14 compress missingok notifempty create 0644 root root } EOF # Monitor log for errors tail -f /var/log/backup.log grep -i "error\|fail\|warn" /var/log/backup.log | tail -20
#!/bin/bash
# cron-check.sh -- quick diagnostic for cron problems
echo "========================================"
echo " Cron Diagnostic -- $(hostname)"
echo " $(date)"
echo "========================================"
echo ""
echo "--- CRON DAEMON STATUS ---"
systemctl is-active crond 2>/dev/null || systemctl is-active cron 2>/dev/null
systemctl status crond 2>/dev/null | grep -E "Active|since" || \
systemctl status cron 2>/dev/null | grep -E "Active|since"
echo ""
echo "--- ROOT CRONTAB ---"
crontab -l 2>/dev/null || echo "(no root crontab)"
echo ""
echo "--- /etc/cron.d/ FILES ---"
ls -la /etc/cron.d/ 2>/dev/null
echo ""
echo "--- RECENT CRON LOG ENTRIES ---"
journalctl -u crond --since "1 hour ago" --no-pager 2>/dev/null | tail -20 || \
grep CRON /var/log/syslog 2>/dev/null | tail -20 || \
tail -20 /var/log/cron 2>/dev/null
echo ""
echo "--- CRON.DAILY SCRIPTS ---"
ls -la /etc/cron.daily/
echo ""
echo "--- CRON.WEEKLY SCRIPTS ---"
ls -la /etc/cron.weekly/
echo "========================================"
| Symptom | Likely Cause | Fix |
|---|---|---|
| No CMD in cron log | Wrong syntax, wrong crontab, crond not running | Check crontab -l, systemctl status crond, verify syntax |
| CMD logged but nothing happens | Script fails silently, PATH issue, permission denied | Add logging, use full paths, check chmod +x |
| Command not found | Command not in cron's PATH | Use full path or set PATH= at top of crontab |
| Permission denied | Script not executable or not readable by cron user | chmod +x script, check file ownership |
| Works manually, fails in cron | Environment difference, PATH, or missing sourced files | Test with minimal env, use full paths, source profile in script |
| % causes weird behavior | Unescaped percent sign in crontab | Escape as \% or move command to script |
| File not found in script | Relative path, wrong working directory | Use absolute paths, add cd "$(dirname "$0")" |
| Script in cron.daily not running | Has .sh extension, not executable, or wrong permissions | Remove extension, chmod +x, check run-parts behavior |
| cron.d job not running | File is world-writable or not owned by root | chmod 644, chown root:root |
| Job overlaps with itself | Previous run still going when next fires | Add lockfile with flock or pidfile check |
| Command | What it checks |
|---|---|
| systemctl status crond | Is cron daemon running? |
| crontab -l | What jobs are scheduled for current user? |
| crontab -l -u alice | What jobs for a specific user? |
| journalctl -u crond --since today | Today's cron activity |
| grep CRON /var/log/syslog | Cron log on Debian/Ubuntu |
| tail -f /var/log/cron | Follow cron log on RHEL |
| ls -la /etc/cron.d/ | System drop-in jobs and permissions |
| ls -la /etc/cron.daily/ | Daily scripts and executability |
| sudo -u alice env -i /bin/sh script.sh | Run as cron user with minimal env |
| * * * * * env > /tmp/cron-env.txt | Capture cron's actual environment |