When Cron Jobs Don't Run - A Troubleshooting Checklist

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.

  1. The cron daemon isn't running
  2. No run entry in the logs (it was never started)
  3. Wrong PATH (cron is a different environment from your interactive shell)
  4. Missing environment variables (.bashrc is not read)
  5. Schedule syntax, permissions, or line-ending mistakes

Assumptions (target environment)

  • OS: Ubuntu / Debian (package cron, log at /var/log/syslog)
  • RHEL / CentOS use service crond and 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 / .profile and 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/syslog shows 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/bin and 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 because LANG or 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. write date +%Y-%m-%d as date +\%Y-\%m-\%d.
  • Both day and weekday set: 0 0 1 * 1 runs on "the 1st or Monday" (an OR), not "the 1st and Monday." This easily diverges from intent.
  • No trailing comments: * * * * * cmd # note is 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.

Checklist Summary

Conclusion: Work top-down — daemon, logs, PATH, environment, syntax, permissions, output — and you can pin down almost any "cron won't run" cause.

Check each item in order.

  • [ ] systemctl status cron (RHEL: crond) shows active (running)
  • [ ] grep CRON /var/log/syslog has a run entry
  • [ ] Commands use absolute paths, or PATH= is defined at the top of the crontab
  • [ ] Required environment variables (LANG, runtime ones) are exported in the script
  • [ ] Five time fields, % escaped, day/weekday OR understood
  • [ ] Script is executable, shebang is correct, line endings are LF
  • [ ] >> /tmp/job.log 2>&1 makes errors visible

Next reading: