Cron Job Not Running? Work Through This Checklist
The command runs perfectly when you type it. From cron it does nothing at all. Here is the order to check things in.
Updated 5 August 2026 · 9 min read
-
Written by
Andrian Valeanu
Founder of Pulsetic
-
Reviewed by
Ionut Caval
Technical reviewer
Check whether cron logged an attempt: grep CRON /var/log/syslog. No log line means the daemon, the schedule or the crontab is the problem. A log line but no result means the script ran and failed, and the usual cause is cron's minimal environment, since it does not read your shell profile and PATH is typically only /usr/bin:/bin.
Key takeaways
- Start by asking whether cron even tried. The log answers that in one command and halves the search.
- Cron does not read
.bashrcor.profile. Absolute paths for the interpreter and the script fix most failures. - Jobs start in the owner's home directory, not the script's directory.
- An unescaped
%silently truncates the command at that point. - Redirect output before debugging anything else, or you are guessing without evidence.
First: did cron even try?
Everything else depends on this answer, so establish it before changing anything. One command splits the problem into two much smaller ones.
# Debian / Ubuntu
grep CRON /var/log/syslog | tail -20
# RHEL / Rocky / Fedora
sudo tail -20 /var/log/cron
If you see a CMD line with your command, cron launched it and the fault is in the script or its environment: skip to step 4. If there is nothing, cron never tried, and the fault is in the schedule, the crontab or the daemon: start at step 1. Full detail on reading these files is in where cron logs are stored.
1. Is the cron daemon actually running?
Rare on a normal server, common in containers, where no init system starts it and nothing tells you.
systemctl status cron # Debian / Ubuntu
systemctl status crond # RHEL / Rocky / Fedora
# Not running? Start and enable it
sudo systemctl enable --now cron
2. Is the job in the crontab you are reading?
Cron jobs live in several places and each is owned by a different user, so a job can be perfectly installed somewhere you are not looking. A frequent version of this: the job was added with sudo crontab -e, which edits root's crontab, and then looked for with plain crontab -l, which shows yours.
crontab -l # your own
sudo crontab -l # root's
sudo crontab -l -u www-data # another user's
sudo cat /etc/crontab # system-wide (has a user column)
sudo ls -l /etc/cron.d/ # package-installed jobs
sudo ls -l /etc/cron.{hourly,daily,weekly,monthly}/
Entries in /etc/crontab and /etc/cron.d/ take a sixth column naming the user to run as. Paste a five-field user crontab line into one of those files and cron reads your command name as the username, and the job never runs.
3. Does the schedule mean what you think?
Two traps here. The first is simple miscounting, which the cron expression generator settles in a few seconds by translating any expression into plain English and previewing its next runs.
The second is documented cron behaviour that reads backwards. When both the day-of-month and day-of-week fields are restricted, cron runs the job when either matches, not both. So 0 0 13 * 5 is not "Friday the 13th", it is the 13th of every month and every Friday.
Then check the clock the schedule is measured against. Cron uses the system timezone, and containers very often run UTC while you are thinking in local time. timedatectl or date will tell you which one you are actually on.
4. The environment is not your shell
This is the single most common cause of a job that runs by hand and not from cron. Cron gives jobs a deliberately bare environment: it does not read .bashrc, .bash_profile or .profile, so your aliases, exported variables and PATH additions do not exist. PATH is typically just /usr/bin:/bin.
Anything installed by a version manager (nvm, pyenv, rbenv, asdf) is invisible, because those work by putting shims on a PATH that cron never sets up. The result is a job that fails instantly with "command not found", written to output you are probably discarding.
# Fails: cron has no idea what "python3" is
0 3 * * * python3 /opt/app/task.py
# Works: absolute path to the interpreter and the script
0 3 * * * /usr/bin/python3 /opt/app/task.py
# Set what the job needs at the top of the crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SHELL=/bin/bash
0 3 * * * /opt/app/task.sh
To find the absolute path of a command, run which python3 in your terminal and use whatever it prints.
5. The working directory is not the script's directory
Cron starts every job in the owner's home directory. A script that opens config.json or writes ./output/report.csv resolves those relative to /home/you, not to wherever the script lives. Either use absolute paths inside the script, or change directory first:
0 3 * * * cd /var/www/app && ./task.sh >> /var/log/task.log 2>&1
Stop finding out weeks later
Cron job monitoring alerts you when a scheduled job fails to check in, including jobs that never started at all.
6. Permissions and the shebang
If you call a script directly rather than passing it to an interpreter, it needs to be executable and it needs a shebang line telling the kernel what to run it with.
chmod +x /opt/app/task.sh
head -1 /opt/app/task.sh # should read #!/bin/bash or similar
Also confirm the user the job runs as can actually read the script and write wherever it writes. A job in root's crontab writing to a file owned by root works; the same line in your crontab may not.
7. The percent sign
Inside a crontab, % is special. The first one ends the command, and everything after it is passed to the job as standard input with each further % becoming a newline. This breaks the most natural thing in the world, putting a date in a filename, and it fails silently rather than reporting an error.
# Truncated at the % : the command becomes "date +"
0 3 * * * /opt/app/backup.sh > /backups/db-$(date +%Y-%m-%d).sql
# Correct: escape every percent sign
0 3 * * * /opt/app/backup.sh > /backups/db-$(date +\%Y-\%m-\%d).sql
8. Crontab syntax errors
Two error messages account for most rejected crontabs. "errors in crontab file, can't install" usually means the file does not end with a newline, and the last entry is the one that silently disappears. "bad minute" means the first field is not a valid minute expression, most often because the command was written with only four time fields, or because a stray space split one field into two.
Editing with crontab -e validates the file before installing it, which is why installing a crontab from a file with crontab myfile is the riskier path.
9. Run it exactly as cron would
Testing in your own shell proves very little, because your shell is the thing that differs. Reproduce cron's environment instead:
# Run with an empty environment, the way cron effectively does
env -i /bin/sh -c '/usr/bin/python3 /opt/app/task.py'
# Or dump cron's real environment once, then compare
* * * * * env > /tmp/cron-env.txt
# Run as the job's user, without your shell profile
sudo -u www-data -i /opt/app/task.sh
The env trick is worth doing once on any machine where cron misbehaves. Remove the line after it has fired.
10. Overlapping runs
Cron does not check whether the previous run of a job has finished. Schedule something every five minutes that occasionally takes eight, and you get two copies running at once, then three. The symptom is rarely "not running" and more often duplicated work, a locked database table, or a machine slowly grinding to a stop. Wrap the command in flock to prevent it:
*/5 * * * * /usr/bin/flock -n /tmp/task.lock /opt/app/task.sh >> /var/log/task.log 2>&1
The -n flag makes a run exit immediately if the lock is held rather than queueing behind it.
The failure this checklist cannot catch
Everything above assumes you already know something is wrong. That is the easy case. The expensive one is the job that stopped weeks ago and told nobody, because cron reports nothing on success and nothing on failure, and those two silences are indistinguishable.
The way out is to stop relying on cron to speak up. With cron job monitoring the job pings a URL when it completes, and a missing ping raises an alert. Because the trigger is the absence of a signal, it catches the whole class of problems in this article at once: the daemon that was never started in a container, the crontab lost during a migration, the job that hangs on a network call, and the script that has been exiting with an error every night since a dependency changed.
Every item on this checklist is something you go looking for. Monitoring is what tells you to start looking.
See how Pulsetic's cron job monitoring catches this from the outside, across 15+ locations.
Frequently asked questions
-
Why does my cron job work manually but not from cron?
Almost always the environment. Cron does not read .bashrc or .profile, so your PATH and exported variables do not exist and commands cannot be found. Use the absolute path to both the interpreter and the script, and set any variables the job needs at the top of the crontab.
-
How do I know if cron even tried to run my job?
Check the system log: "grep CRON /var/log/syslog" on Debian and Ubuntu, or /var/log/cron on RHEL and Fedora. A CMD line containing your command means cron launched it. No line means cron never tried, which points at the daemon, the schedule or the crontab instead.
-
What does "errors in crontab file, can't install" mean?
Usually that the file does not end with a newline character, so the final entry is incomplete. It can also mean a malformed time field. Editing with "crontab -e" validates the file before installing it and avoids both.
-
Can two copies of the same cron job run at once?
Yes. Cron does not check whether the previous run has finished, so a job scheduled every five minutes that sometimes takes eight will overlap itself. Wrap the command in "flock -n /tmp/job.lock" so a new run exits immediately when one is already in progress.
-
Catch the next outage before your visitors do.
2-minute setup · Cancel any time
-
No credit card needed