When Cron Jobs Don't Run - A Troubleshooting Checklist
What You'll Learn
- Why a script that runs by hand fails under cron
- How to confirm cron failures from the logs
- How to work through the usual traps: PATH, environment, schedule syntax, and permissions
Quick Summary (triage order)
Almost every "cron won't run" case collapses to one of these five. Check them top-down.
- The cron daemon isn't running
- No run entry in the logs (it was never started)
- Wrong PATH (cron is a different environment from your interactive shell)
- Missing environment variables (
.bashrcis not read) - Schedule syntax, permissions, or line-ending mistakes
Assumptions (target environment)
- OS: Ubuntu / Debian (package
cron, log at/var/log/syslog) - RHEL / CentOS use service
crondand log at/var/log/cron - Focus is the user crontab (
crontab -e)
Why does it run by hand but not under cron?
Conclusion: Unlike an interactive login, cron runs a non-interactive shell that never reads
.bashrc/.profileand keeps a minimal PATH. That environment gap explains most "works manually, fails in cron" cases.
A manual run (login shell) and a cron run start the shell through fundamentally different paths.
| Item | Interactive login shell | Cron job |
|---|---|---|
| Startup files read | .bash_profile / .bashrc, etc. |
none |
PATH |
full (includes /usr/local/bin, etc.) |
minimal (often /usr/bin:/bin) |
HOME / LOGNAME |
set | partially set |
| Working directory | wherever you are | the user's $HOME |
| Standard output | terminal | mailed (or discarded) |
Once you internalize this gap, every check below becomes "fill in what the minimal cron environment is missing," one item at a time.
Is the cron daemon running?
Conclusion: First confirm the daemon with
systemctl status cron. If it is stopped, no job runs at all. This is the precondition to rule out before anything else.
# Ubuntu / Debian $ systemctl status cron # RHEL / CentOS $ systemctl status crond
If it is not active (running), enable and start it.
$ sudo systemctl enable --now cron
The service name differs by distro: Ubuntu uses cron, RHEL-family uses crond. If status says Unit cron.service could not be found, try the other name.
How do I confirm whether it ran from the logs?
Conclusion: Cron logs to syslog every time it fires. If
grep CRON /var/log/syslogshows no entry, the job was never started in the first place.
Whether a log line exists or not changes the direction of your triage.
# Ubuntu / Debian $ grep CRON /var/log/syslog | tail -20 # Via the systemd journal $ journalctl -u cron --since "1 hour ago" # RHEL / CentOS $ sudo grep CRON /var/log/cron | tail -20
A successful trigger leaves a line like this.
Jun 5 10:00:01 host CRON[12345]: (alice) CMD (/home/alice/backup.sh)
How to read it:
- An entry exists → cron fired. The problem is in the script (PATH / permissions / environment). Move on to the next sections.
- No entry → it was never started. Suspect a schedule-syntax mistake, the wrong crontab location, or a stopped daemon.
If no log appears at all on Ubuntu, rsyslog may be missing or stopped (systemctl status rsyslog). In that case use journalctl -u cron as your primary source.
Why does PATH cause it to fail?
Conclusion: Cron's PATH is minimal (often
/usr/bin:/bin). Write commands in/usr/local/binand similar with absolute paths, or define PATH at the top of the crontab.
"Works by hand but command not found under cron" almost always comes from this. There are three fixes.
# 1) Use an absolute path (find it with which) $ which node /usr/local/bin/node # In the crontab, specify the absolute path * * * * * /usr/local/bin/node /home/alice/job.js
# 2) Define PATH at the top of the crontab PATH=/usr/local/bin:/usr/bin:/bin 0 * * * * node /home/alice/job.js
# 3) Export PATH inside the script before doing work #!/bin/bash export PATH=/usr/local/bin:/usr/bin:/bin node /home/alice/job.js
Capturing the real cron environment is the surest method. Temporarily add the line below, then diff the resulting cronenv against your shell.
* * * * * env > /tmp/cronenv 2>&1
$ diff <(env) /tmp/cronenv
Is a missing environment variable the cause?
Conclusion: Cron does not read
.bashrc/.profile. Jobs often fail becauseLANGor runtime variables (NODE_ENV,JAVA_HOME, etc.) are unset.
Variables that your interactive shell set implicitly are empty under cron. Defining them explicitly in the script is the most reliable fix.
#!/bin/bash # Variables that tend to be unset in the cron environment export LANG=en_US.UTF-8 export HOME=/home/alice export NODE_ENV=production cd "$HOME/app" || exit 1 /usr/local/bin/node index.js
Sourcing ~/.bashrc inside a cron script as a workaround is fragile. The "return immediately if non-interactive" guard at the top of .bashrc (Ubuntu default) can stop anything from loading. Export the variables you need individually.
How do I find schedule and syntax mistakes?
Conclusion: There are five fields: minute, hour, day-of-month, month, day-of-week. The day/weekday OR behavior and unescaped
%are the classic traps. If no run entry appears in the log, suspect the syntax first.
┌── minute (0-59) │ ┌── hour (0-23) │ │ ┌── day of month (1-31) │ │ │ ┌── month (1-12) │ │ │ │ ┌── day of week (0-7, 0 and 7 are Sunday) │ │ │ │ │ * * * * * command to run
Common stumbles:
%is not literal: cron turns%in a command into a newline. Escape it with a backslash, e.g. writedate +%Y-%m-%dasdate +\%Y-\%m-\%d.- Both day and weekday set:
0 0 1 * 1runs on "the 1st or Monday" (an OR), not "the 1st and Monday." This easily diverges from intent. - No trailing comments:
* * * * * cmd # noteis invalid. Comments must be on their own#line.
crontab.guru is a fast way to verify what a schedule means. After saving, always re-check with crontab -l that it was stored as intended.
Check permissions and the crontab location
Conclusion: A script without execute permission or with a wrong shebang path fails right after it fires. When placing files in
/etc/cron.d/, it's easy to forget that the line needs a user field.
Script execute permission and shebang
$ chmod +x /home/alice/backup.sh $ head -1 /home/alice/backup.sh #!/bin/bash
A script edited on Windows can pick up CRLF line endings and fail with bad interpreter. See Fixing "bad interpreter" for details.
The syntax differs by crontab type
| Location | User field | Use |
|---|---|---|
crontab -e (user) |
none | personal jobs |
/etc/crontab |
required | system-wide |
/etc/cron.d/<file> |
required | package / extra jobs |
# /etc/cron.d/ and /etc/crontab require a user field # ┌min ┌hr ┌day ┌mon ┌dow ┌user ┌command 0 3 * * * alice /home/alice/backup.sh
Get the user field wrong and the log shows an error such as Unauthorized, or the job silently never runs.
How do I redirect output to debug?
Conclusion: Cron mails standard output and standard error. On hosts without an MTA that output vanishes, so redirecting to a file makes the errors visible.
# Send stdout and stderr to a log file * * * * * /home/alice/backup.sh >> /tmp/backup.log 2>&1
2>&1 folds standard error into the same file. After it runs, /tmp/backup.log keeps the raw command not found or Permission denied messages.
Minimal reproduction
When you can't narrow the cause, first confirm cron itself works with a job that only writes the date every minute.
* * * * * date >> /tmp/cron-test.log 2>&1
If this log grows, cron is healthy and you've isolated the problem to the script.