← trebben.dk

Why your cron job isn't running

March 2026 · A debugging checklist

Your cron job should be running. It isn't. You've stared at the crontab line and it looks correct. Here's how to figure out what's actually wrong, in order from most likely to least obvious.

Check 1

Is cron actually running?

Before debugging your job, make sure the cron daemon itself is alive.

# systemd-based systems (Ubuntu, Debian, CentOS 7+):
systemctl status cron
# or on some systems:
systemctl status crond

# Older init systems:
service cron status

If cron isn't running, start it: systemctl start cron. If it isn't enabled at boot: systemctl enable cron. On Docker containers, cron is usually not running by default — you need to start it explicitly in your entrypoint.

Check 2

Is the crontab actually installed?

# Show crontab for the current user:
crontab -l

# Show crontab for a specific user:
sudo crontab -u www-data -l

# Show the system crontab:
cat /etc/crontab

# Show cron.d entries:
ls -la /etc/cron.d/

Common mistake: editing /etc/crontab when your job is in crontab -e, or vice versa. They're different files. Also: /etc/crontab has an extra user field that per-user crontabs don't. If you copy a line from one to the other without adjusting, it won't parse.

# /etc/crontab format (has username field):
0 * * * * root /usr/local/bin/backup.sh

# crontab -e format (no username field):
0 * * * * /usr/local/bin/backup.sh

Check 3

Is the cron expression correct?

Read it out loud. Cron fields are:

minute  hour  day-of-month  month  day-of-week
  0      9        *            *        1-5

Common traps:

Check 4

Is it a PATH problem?

This is the #1 cause of "works when I run it manually, fails in cron." Cron runs with a minimal PATH, usually just /usr/bin:/bin. Your interactive shell has a much richer PATH.

# See what PATH cron uses — add this as a temporary cron job:
* * * * * env > /tmp/cron-env.txt

# Wait a minute, then check:
grep PATH /tmp/cron-env.txt

Fix it by using absolute paths in your commands:

# Bad — relies on PATH:
0 * * * * backup.sh

# Good — absolute path:
0 * * * * /usr/local/bin/backup.sh

# Or set PATH at the top of your crontab:
PATH=/usr/local/bin:/usr/bin:/bin
0 * * * * backup.sh

Check 5

Is it a permissions problem?

# Is the script executable?
ls -la /usr/local/bin/backup.sh

# Does the cron user have access to required files/dirs?
sudo -u www-data /usr/local/bin/backup.sh

# Check /etc/cron.allow and /etc/cron.deny:
cat /etc/cron.allow 2>/dev/null
cat /etc/cron.deny 2>/dev/null

If /etc/cron.allow exists, only users listed in it can use cron. If it doesn't exist but /etc/cron.deny does, anyone not listed in deny can use cron.

Check 6

Is it an environment problem?

Cron doesn't load .bashrc, .profile, or .bash_profile. Environment variables you set in those files don't exist in cron.

# If your script needs environment variables, set them explicitly:
0 * * * * DB_HOST=localhost DB_PASS=secret /usr/local/bin/etl.sh

# Or source them in the crontab:
0 * * * * . /home/deploy/.env && /usr/local/bin/etl.sh

# Or source them in the script itself:
#!/bin/bash
source /home/deploy/.env
# ... rest of script

This also affects nvm, pyenv, rbenv, and any tool that relies on shell initialization. If your cron job uses node, python3, or ruby installed via a version manager, use the full path to the binary.

Check 7

Check the logs

# Most systems log cron to syslog:
grep CRON /var/log/syslog | tail -20

# On CentOS/RHEL:
grep CRON /var/log/cron | tail -20

# If using journald:
journalctl -u cron --since "1 hour ago"

Look for your job in the logs. If it's there, cron ran it — the problem is inside the script. If it's not there, cron didn't run it — go back to checks 1-3.

Also: cron emails output to the user by default. If you've redirected stderr to /dev/null, you're hiding errors.

# This hides all output — including errors:
0 * * * * /usr/local/bin/backup.sh > /dev/null 2>&1

# Better: log errors to a file:
0 * * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Check 8

Is it a newline problem?

This one catches everyone once. Cron requires a newline at the end of the crontab file. If the last line of your crontab doesn't end with a newline character, that line is silently ignored.

# Check if your crontab ends with a newline:
crontab -l | tail -c 1 | xxd

If you see 0a, you're fine. If there's no output, add an empty line at the end of your crontab.

Check 9

Is it a timezone problem?

# What timezone is your system in?
timedatectl | grep "Time zone"
# or:
cat /etc/timezone

# What time does cron think it is?
* * * * * date > /tmp/cron-time.txt

If your server is in UTC and your crontab says 0 9 * * *, that's 9 AM UTC — not your local time. After DST changes, your "9 AM" job might run at 8 AM or 10 AM local time. Use UTC everywhere and convert in your head.

Still stuck?

The nuclear debugging option: create a cron job that runs every minute, writes to a file, and confirms cron itself is working.

* * * * * echo "cron works at $(date)" >> /tmp/cron-test.txt

Wait two minutes. If /tmp/cron-test.txt has entries, cron is working — the problem is specific to your job. If it doesn't, the problem is with cron itself (go back to check 1).

Prevent next time

The real problem with cron failures is discovery time. A job can silently stop running for days or weeks before anyone notices. Heartbeat monitoring fixes this: your job pings an endpoint each time it succeeds. If the ping stops arriving, you get alerted immediately.

# Add heartbeat monitoring to any cron job:
0 * * * * /usr/local/bin/backup.sh && curl -s https://ping.trebben.dk/p/your-token

CronPulse does this. Free tier, no agents to install, just append && curl to your crontab line. You'll know within minutes if a job stops running, instead of finding out from a customer.

Related: Five ways cron jobs fail →  ·  Cron expression examples →  ·  ← trebben.dk