Blog

Cron job not running? 12 causes and how to fix each one

Your crontab entry looks right, the command works in your terminal, and nothing happens. Here are the 12 reasons cron jobs fail to run, how to check each one, and how to find out next time before anyone else does.

Published on September 19, 2026

The command works when you paste it into your terminal. The crontab line looks correct. And yet the backup file never appears, the report never lands in anyone’s inbox, and there is nothing in any log you can find.

This is the first post in our Cron in production series. It starts with the most common question of all: why is my cron job not running? Below are the 12 causes we see most often, roughly in the order you should check them, with the command that confirms or rules out each one.

First: did it not run, or did it run and fail?

These are two different problems, and people lose hours by mixing them up. Before anything else, find out whether cron even tried to start your job.

On Debian and Ubuntu, cron logs every command it starts to syslog:

grep CRON /var/log/syslog
# or, on systems with journald only:
journalctl -u cron --since "1 hour ago"

On RHEL, Rocky, Alma and Fedora the daemon is called crond and logs to its own file:

grep CROND /var/log/cron
journalctl -u crond --since "1 hour ago"

If you see a CMD (...) line with your command at the expected time, cron did its part and your job failed on its own. Skip to causes 4 to 9 and cause 12. If there is no such line, cron never started it, and causes 1 to 3 and 10 to 11 are the likely ones.

1. The cron daemon is not running

Sounds too obvious to check, which is exactly why it gets skipped. Minimal server images, fresh VPS templates and almost every Docker image ship without a running cron daemon.

systemctl status cron     # Debian, Ubuntu
systemctl status crond    # RHEL family
pgrep -a cron             # anywhere, including containers

If it is not running, start it and make sure it comes back after a reboot with systemctl enable --now cron. Inside a container, nothing starts cron for you: it has to be the main process or run under a process supervisor. We will cover that properly in a later post in this series.

2. The schedule does not mean what you think

Five fields, in this order: minute, hour, day of month, month, day of week. The classic mistakes:

Paste your expression into our cron expression parser and check the next run times it lists. If they are not what you expected, the problem is solved before you touch anything else.

3. You edited a different crontab

A single machine has several places where cron jobs live, and they do not all use the same format:

# user crontab (crontab -e): no user field
0 2 * * * /opt/scripts/backup.sh

# /etc/cron.d/backup: user field required
0 2 * * * root /opt/scripts/backup.sh

Copy a line from one format to the other without adjusting it and it breaks: in /etc/cron.d your script path is read as the user name, and in a user crontab cron tries to run a command called root. List what is actually installed with crontab -l and sudo crontab -l, and read through /etc/cron.d/.

4. PATH is not your shell’s PATH

This is the most common cause of “it works in my terminal”. Cron starts jobs with a minimal environment, usually PATH=/usr/bin:/bin. Anything installed in /usr/local/bin, /snap/bin, ~/.local/bin or a version manager directory is simply not found.

The fix is either absolute paths everywhere, or a PATH line at the top of the crontab:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

0 2 * * * node /opt/app/scripts/cleanup.js

Use command -v node in your normal shell to find the full path of a binary.

5. Your environment variables are missing

Cron does not read .bashrc, .profile or .bash_profile. Every variable you exported there (database URLs, API keys, NODE_ENV, JAVA_HOME) does not exist when the job runs.

You can define variables directly in the crontab, the same way as PATH above. Or keep them in a file your script loads itself:

0 2 * * * . /opt/app/.env.cron && /opt/app/scripts/report.sh

The quickest way to see exactly what cron gives your job is to ask it. Add a temporary entry, wait a minute, then remove it:

* * * * * env > /tmp/cron-env.txt

Compare /tmp/cron-env.txt with the output of env in your terminal and the missing piece is usually obvious.

6. The job runs under /bin/sh, not bash

Cron runs commands with /bin/sh unless told otherwise. On Debian and Ubuntu that is dash, which does not understand bash features like [[ ]], arrays, source or {1..10}. The job fails with a syntax error that nobody sees.

Either set the shell at the top of the crontab:

SHELL=/bin/bash

or, better, put the logic into a script that starts with #!/usr/bin/env bash and call the script from cron.

7. An unescaped percent sign cuts your command short

In a crontab line, % is special: cron converts it into a newline and feeds everything after the first one to the command as standard input. This breaks the very common pattern of date-stamped filenames:

# broken: cron sees the command end at "+"
0 2 * * * pg_dump mydb > /backups/db-$(date +%F).sql

# works
0 2 * * * pg_dump mydb > /backups/db-$(date +\%F).sql

Escape every % with a backslash, or move the command into a script where the rule does not apply.

8. Permissions, missing shebang or Windows line endings

If cron calls a script directly, the script needs to be executable and start with a shebang line:

chmod +x /opt/scripts/backup.sh
head -1 /opt/scripts/backup.sh   # should print something like #!/usr/bin/env bash

Two quieter variants of the same problem. A script edited on Windows may have \r\n line endings, and the kernel then looks for an interpreter literally named bash\r. Check with file backup.sh (it will say “with CRLF line terminators”) and fix with sed -i 's/\r$//' backup.sh. And the job may run as a different user than you tested with, one that cannot read the config file or write to the output directory. Our chmod calculator helps when you need to work out the right mode.

9. The last line has no newline, or the cron.d filename is wrong

Two rules that trip up almost everyone who deploys cron files with configuration management or a Dockerfile:

tail -c1 /etc/cron.d/backup | xxd   # should end with 0a
ls -l /etc/cron.d/

10. The user is not allowed to use cron

If /etc/cron.allow exists, only users listed in it can have a crontab. If it does not exist but /etc/cron.deny does, everyone except the users listed there can. Hardened images sometimes ship with restrictive versions of these files. crontab -e will tell you when you are denied, but a crontab installed by other means may just sit there unused.

11. The machine was off, asleep, or in another time zone

Cron runs jobs at the time they are due, and only then. If the server was rebooting at 02:00, or the laptop was asleep, that run is gone for good; cron does not catch up on missed runs. For machines that are not always on, anacron or systemd timers with Persistent=true are the right tool.

Time zones cause the other half of “it never ran when I expected”. Cron uses the system time zone, and most servers are set to UTC. A job scheduled for 0 9 * * * on a UTC server runs at 09:00 UTC, which is 11:00 in Warsaw in summer and 04:00 in New York. Check with timedatectl. Some cron implementations, cronie among them, support a CRON_TZ variable for per-crontab time zones; check man 5 crontab on your system before you rely on it. Daylight saving time transitions add their own surprises, which get a dedicated post later in this series.

12. It ran, failed, and the error went nowhere

By default cron emails a job’s output to the crontab owner. On most modern servers there is no mail transfer agent installed, so that output, including every error message, is quietly discarded. The job fails every night and there is no trace of why.

At minimum, send output somewhere you can read it:

0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

How to test like cron, before cron does

Most of the causes above come down to “cron’s environment is not your terminal’s environment”. You can reproduce cron’s conditions directly, without waiting for the next scheduled run:

env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME="$HOME" LOGNAME="$USER" \
  /bin/sh -c '/opt/scripts/backup.sh'

If the script fails here, it will fail under cron, and you get to see the error message right away.

The problem no checklist solves: finding out

Every cause in this post has the same thing in common. Nothing tells you. The job does not throw an error at you, it just stops happening, and you find out days later when someone needs the backup, the invoice run, or the report.

Logging the output (cause 12) helps you diagnose a failure after you know about it. It does not help you notice that the job never started, because a job that does not run does not write anything to its log. For that you need something that expects to hear from the job, and raises an alarm when it does not.

That is what heartbeat monitoring does, and it is what we built HelloCron for. The job sends a ping when it starts and when it finishes, and HelloCron alerts you if a ping arrives with an error, or if the expected ping never arrives at all:

HELLOCRON_API_KEY=your_api_key

0 2 * * * curl -fsS -H "Authorization: Bearer $HELLOCRON_API_KEY" "https://api.hellocron.com/ping/nightly-backup?status=run"; /opt/scripts/backup.sh && curl -fsS -H "Authorization: Bearer $HELLOCRON_API_KEY" "https://api.hellocron.com/ping/nightly-backup?status=complete" || curl -fsS -H "Authorization: Bearer $HELLOCRON_API_KEY" "https://api.hellocron.com/ping/nightly-backup?status=fail"

Or, with the HelloCron CLI, which captures the exit code and the error output for you (note the absolute path, see cause 4):

0 2 * * * /usr/local/bin/hellocron run nightly-backup /opt/scripts/backup.sh

The walkthrough in Monitor a cron job in 60 seconds takes you through the full setup. The free plan covers 15 monitors, which is enough for most servers.

Quick checklist

  1. Check the cron log: did it start at all?
  2. Is the cron daemon running and enabled?
  3. Do the next run times in the cron parser match your expectation?
  4. Is it in the crontab you think it is, in the right format?
  5. Absolute paths or a PATH line?
  6. Environment variables defined for cron, not just in .bashrc?
  7. Bash features with SHELL=/bin/bash or a proper shebang?
  8. Every % escaped?
  9. Executable, Unix line endings, readable by the user cron runs it as?
  10. Trailing newline, no dots in /etc/cron.d filenames?
  11. Allowed by cron.allow and cron.deny?
  12. Right time zone, and was the machine actually on?

Next in the series: Where are cron logs? covers where the cron daemon logs on Ubuntu, Debian, RHEL, Alpine and inside containers, and how to make your jobs’ output easy to find.

← Back to blog

Start monitoring in 60 seconds

Sign up, get an API key, paste the curl into your cron. That's it.