Debugging Cron Jobs That Never Run: A Systematic Checklist
by Sinthuyan · April 10, 2026
The most frustrating cron failure is the one that produces no output, no error, and no log entry. The job simply doesn't run. After investigating dozens of these over the years, I've developed a systematic checklist that resolves the problem in almost every case. Work through these steps in order — the first few catch 80% of issues.
Step 1: Validate the Expression Syntax
Use a cron expression validator before assuming the scheduler is broken. Common syntax errors include: 0 9 * * 1-7 (day-of-week range 1-7 is incorrect — should be 1-5 for weekdays or * for every day; day 7 equals Sunday in most implementations but some reject it), extra spaces between fields, and fields with values outside the valid range. Paste the expression into a validator and check that the "next run times" match your intent.
Step 2: Check the Timezone
Run date on the server to confirm the system clock timezone. Cron evaluates expressions against the server's local time by default. If the server is UTC but you expected EST, your "9 AM" job actually runs at 2 PM or 1 PM depending on DST. This is the single most common cause of a job appearing to run at the wrong time or "not at all" for the first several hours after deployment.
To set a per-crontab timezone on GNU cron, add CRON_TZ=America/New_York as the first line of the crontab. Verify with crontab -l.
Step 3: Verify the Cron Daemon Is Running
A stopped cron daemon silently misses all scheduled jobs. Check with systemctl status cron (Debian/Ubuntu) or systemctl status crond (RHEL/CentOS). If inactive, start it with systemctl start cron and enable it to start on boot with systemctl enable cron.
Step 4: Check Output Redirection
By default, cron emails job output to the local user. If the system has no mail transfer agent configured (common on cloud VMs), that output is silently discarded. Errors are lost. Add explicit output redirection to your crontab entries:
- Discard all output:
0 2 * * * /path/to/job.sh >/dev/null 2>&1 - Log to file:
0 2 * * * /path/to/job.sh >>/var/log/myjob.log 2>&1
Until you add logging, you may never see errors that prevent the job from completing.
Step 5: Check File Permissions
The script must be executable: chmod +x /path/to/job.sh. Cron runs as the user whose crontab is being processed. If the script is owned by root but the crontab belongs to www-data, the job will fail with a permission error — silently, if output redirection isn't configured.
Step 6: Audit Environment Variables
Cron runs with a minimal environment: typically only HOME, LOGNAME, PATH=/usr/bin:/bin, and SHELL=/bin/sh. Scripts that depend on nvm, pyenv, or custom PATH entries added in .bashrc will fail because those initializers don't run in a cron shell. Always use absolute paths to binaries: /usr/bin/python3, /home/ubuntu/.nvm/versions/node/v20.0.0/bin/node. Alternatively, set PATH= explicitly at the top of the crontab.
Step 7: Read the Cron Logs
- Debian/Ubuntu:
grep CRON /var/log/syslog | tail -50 - RHEL/CentOS:
grep CRON /var/log/cron | tail -50 - Systemd:
journalctl -u cron --since "1 hour ago"
Cron logs every job it attempts to start. If the job isn't appearing in logs at the expected time, the daemon isn't running or the expression doesn't match. If it appears but produces no other output, the script is failing silently — go back to Step 4.
Step 8: Run the Script as the Cron User
Reproduce the cron environment manually: sudo -u www-data env -i HOME=/var/www SHELL=/bin/bash PATH=/usr/bin:/bin /path/to/job.sh. The env -i flag strips all environment variables, approximating what cron sees. If the script fails in this context but succeeds interactively, you've found an environment dependency.