🔍 Cron Troubleshooting

Cron & Scheduling Series: Part 1 — Cron Deep Dive  |  Part 2 — Systemd Timers  |  Part 3 — at & anacron  |  Part 4 — Troubleshooting

Why Cron Jobs Fail

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.

The Diagnostic Workflow

  1. Is crond running? — systemctl status crond
  2. Is the job in the crontab? — crontab -l or check /etc/cron.d/
  3. Is the syntax correct? — Verify with crontab.guru
  4. Is it logging anything? — journalctl -u crond or /var/log/cron
  5. Does it run manually? — Run as the cron user with cron's environment
  6. PATH/environment issue? — Use full paths, check env differences
  7. Permission issue? — Check script is executable, user has access
  8. Output going somewhere? — Add logging to the job

Examples

1
Check Cron Logs
### 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:
Apr 25 02:00:01 server CROND[12345]: (root) CMD (/usr/local/bin/backup.sh) Apr 25 02:00:02 server CROND[12345]: (root) CMDOUT (tar: Removing leading...) Apr 25 03:00:01 server CROND[12346]: (alice) CMD (/home/alice/sync.sh) Apr 25 03:00:01 server CROND[12346]: (CRON) error (can't open /home/alice/sync.sh)
💡 The cron log shows CMD entries for every job that fires. If you see a CMD entry but the job appears to have done nothing, the script ran but failed silently. If you see no CMD entry at all for a job that should have run, cron never triggered it — check syntax and crontab location.
2
Test a Script in Cron's Environment

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
⚠️ This is the number one cron fix. If a script works in your shell but not in cron, the cause is almost always the environment. Cron runs with 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.
3
Common Failure: Script Not Executable
# 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
Scripts in /etc/cron.daily/ must be executable and must not have a file extension (no .sh). run-parts — which executes these directories — skips files with extensions by default on many systems.
4
Common Failure: PATH and Command Not Found
# 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
The definitive fix: full paths everywhere. In cron jobs and the scripts they call, always use full absolute paths for every command. /usr/bin/python3 not python3. /usr/local/bin/backup.sh not backup.sh. This eliminates PATH issues entirely.
5
Common Failure: Percent Signs and Special Characters
# 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
⚠️ The percent sign rule catches everyone. In crontab syntax, an unescaped % 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.
6
Common Failure: Relative Paths and Working Directory
# 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
💡 Add 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.
7
Adding Logging to Any Cron Job

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
8
Cron Troubleshooting Checklist Script
#!/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 "========================================"
Run this script first whenever a cron job is reported as not working. It gives you daemon status, what jobs are defined, and recent log activity — the three things that answer 90% of cron questions in under 30 seconds.

Quick Reference

Cron Failure Patterns and Fixes

SymptomLikely CauseFix
No CMD in cron logWrong syntax, wrong crontab, crond not runningCheck crontab -l, systemctl status crond, verify syntax
CMD logged but nothing happensScript fails silently, PATH issue, permission deniedAdd logging, use full paths, check chmod +x
Command not foundCommand not in cron's PATHUse full path or set PATH= at top of crontab
Permission deniedScript not executable or not readable by cron userchmod +x script, check file ownership
Works manually, fails in cronEnvironment difference, PATH, or missing sourced filesTest with minimal env, use full paths, source profile in script
% causes weird behaviorUnescaped percent sign in crontabEscape as \% or move command to script
File not found in scriptRelative path, wrong working directoryUse absolute paths, add cd "$(dirname "$0")"
Script in cron.daily not runningHas .sh extension, not executable, or wrong permissionsRemove extension, chmod +x, check run-parts behavior
cron.d job not runningFile is world-writable or not owned by rootchmod 644, chown root:root
Job overlaps with itselfPrevious run still going when next firesAdd lockfile with flock or pidfile check

Essential Diagnostic Commands

CommandWhat it checks
systemctl status crondIs cron daemon running?
crontab -lWhat jobs are scheduled for current user?
crontab -l -u aliceWhat jobs for a specific user?
journalctl -u crond --since todayToday's cron activity
grep CRON /var/log/syslogCron log on Debian/Ubuntu
tail -f /var/log/cronFollow 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.shRun as cron user with minimal env
* * * * * env > /tmp/cron-env.txtCapture cron's actual environment