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
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
# 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
Read it out loud. Cron fields are:
minute hour day-of-month month day-of-week
0 9 * * 1-5
Common traps:
* * * * * runs every minute, not once0 */2 * * * runs every 2 hours starting at midnight*/30 9-17 * * * runs every 30 minutes from 09:00-17:59, not 09:00-17:00Check 4
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 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
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
# 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
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
# 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.
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).
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