Debugging

Why isn't my cron job running?

A cron job that does not fire is one of the most frustrating problems in ops, because cron itself rarely tells you what is wrong. This is a 10-step checklist for diagnosing the issue, ordered by how often each cause occurs in practice.

1. Verify the syntax

The expression might be valid syntax that means something different from what you intended. Paste it into the explainer to see the plain-English translation and next 5 run times. If the next run is "in 4 days" when you expected "every 5 minutes," the expression is wrong.

Common syntax mistakes:

  • 0 9 * * 1-5 intended as "every weekday at 9 AM" — correct
  • * 9 * * 1-5 means "every minute during the 9 AM hour, on weekdays" — probably not what you wanted
  • */5 * * * 1-5 means "every 5 minutes, every weekday" — note this fires 288 times a day, not once

2. Confirm the crontab is actually loaded

If you edited a file but didn't crontab-import it, the system doesn't know. Verify:

crontab -l            # Shows the active crontab for your user
sudo crontab -u www-data -l   # Check another user's crontab

If your job isn't in the output, install it: crontab my-cron-file. Editing /etc/crontab directly works on some systems but not all — prefer crontab -e per user.

3. Confirm the cron service is running

On systemd-based systems:

systemctl status cron        # Debian/Ubuntu
systemctl status crond       # RHEL/CentOS/Fedora

If it's inactive, start it: sudo systemctl start cron && sudo systemctl enable cron.

Check the cron log for evidence your job actually ran:

grep CRON /var/log/syslog        # Debian/Ubuntu
journalctl -u crond              # systemd

4. PATH and environment variables

Cron runs jobs with a minimal environment — typically just PATH=/usr/bin:/bin and a tiny set of variables. If your script uses python3, node, or anything in /usr/local/bin, it may not find them.

Two fixes:

  1. Set PATH at the top of your crontab:
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
  2. Use absolute paths in your script: /usr/local/bin/python3 instead of python3.

Same for other env vars: if your code reads $AWS_PROFILE or $DATABASE_URL, those won't exist in cron unless you set them explicitly or source a file with them.

5. Permissions on the script

The script must be executable by the user the cron job runs as:

ls -la /path/to/script.sh
# If not executable:
chmod +x /path/to/script.sh

For shell scripts, also confirm the shebang line is correct:

#!/usr/bin/env bash       # portable
#!/bin/bash               # explicit

6. Output is being silently lost

By default, cron emails any output to the user's local mailbox. On most modern systems, there's no local mail setup, so the output just vanishes. Two important fixes:

  1. Capture output to a log file:
    0 9 * * * /path/to/script.sh >> /var/log/my-job.log 2>&1
    The 2>&1 redirects stderr to the same place as stdout, so error messages are visible too.
  2. Set MAILTO at the top of your crontab if you have local mail configured:
    MAILTO="your@email.com"
    0 9 * * * /path/to/script.sh

7. Relative paths fail in cron

Cron starts each job in the user's home directory, not the directory where the script lives. A script that works manually:

./helper.sh    # works from where you ran it
config.json    # works if you cd'd into the right dir

… will fail under cron because ./helper.sh and config.json are resolved relative to /home/user/. Fix it by:

  • Using absolute paths everywhere: /opt/myapp/helper.sh
  • Or cd'ing first: cd /opt/myapp && ./helper.sh

8. Wrong timezone

Cron uses the system's local timezone, which on cloud servers is almost always UTC. So 0 9 * * 1-5 means 9 AM UTC, not 9 AM in your local time.

date              # Shows the system timezone
timedatectl       # On systemd, shows + lets you set the TZ

To run in a specific timezone, either:

  • Set the timezone in your script: TZ='America/New_York' /path/to/script.sh
  • Set CRON_TZ=America/New_York at the top of your crontab (Vixie cron 4.0+)
  • Calculate the equivalent UTC time and use it in the cron expression

This is also where Daylight Saving Time bugs live — see our DST guide.

9. The shell is different

Cron uses /bin/sh by default, which on most Linux systems is dash, not bash. Constructs like [[ ... ]], arrays, and $'' string literals don't work in dash.

Fixes:

  • Set SHELL=/bin/bash at the top of your crontab
  • Or put a proper shebang at the top of your script: #!/usr/bin/env bash

10. The day-field gotcha

If you've set both day-of-month and day-of-week (neither is *), Unix cron fires when EITHER matches — OR semantics. So 0 0 15 * 1 fires on the 15th of every month AND every Monday, not "the 15th if it falls on a Monday."

To get AND semantics, leave one day field as * and gate inside your script:

# Run only on the 15th if it's a weekday
0 0 15 * * [ $(date +\%u) -le 5 ] && /path/to/script.sh

Bonus: a debugging cron job

If a job won't run and you don't know why, install a tiny test job to confirm cron is processing your crontab at all:

* * * * * echo "hi from cron at $(date)" >> /tmp/cron-test.log

Wait two minutes. If /tmp/cron-test.log has entries, cron is working — the problem is in your real job. If the file is empty or doesn't exist, cron itself isn't running your crontab.

Related

Continue reading.