Cron job not running? A troubleshooting checklist
The command works when you run it by hand, the crontab line looks right, and yet nothing happens at 3 AM. Cron rarely says why: it has no dashboard, and by default it mails errors to a local mailbox nobody reads. Go through this checklist from top to bottom. The first items are the most common causes.
1. Is cron running at all?
systemctl status cron # Debian, Ubuntu systemctl status crond # RHEL, Fedora, Rocky, Alma
In a Docker container there is usually no cron daemon unless you start one; see cron jobs in Docker.
2. Did cron try to start the job?
Cron logs every start it makes. If your line isn’t there, cron didn’t try to run it; if it is, the job started and failed on its own.
journalctl -u cron --since "today" # or -u crond grep CRON /var/log/syslog # Debian, Ubuntu with rsyslog sudo tail /var/log/cron # RHEL family
3. The environment is almost empty
This is the classic one. Cron doesn’t read .bashrc or .profile. A job gets SHELL=/bin/sh, a short PATH such as /usr/bin:/bin, and none of your exported variables. So node, php, docker or aws from /usr/local/bin or a version manager simply isn’t found.
- Use absolute paths:
/usr/local/bin/node /srv/app/job.js.command -v nodeshows the path. - Or set the path at the top of the crontab:
PATH=/usr/local/bin:/usr/bin:/bin. - Load secrets explicitly, for example with
set -a; . /srv/app/.env; set +ain a wrapper script.
To reproduce cron’s environment in your shell, run the job with an empty environment:
env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME="$HOME" /bin/sh -c '/srv/app/job.sh'
4. A percent sign cuts the command
In a crontab, % means a new line, and everything after the first one is passed to the command as input. date +%F breaks the line silently. Escape it as \%, or move the command into a script:
0 3 * * * /srv/backup.sh > /var/backups/log-$(date +\%F).txt 2>&1
5. The script can’t be executed
- No execute bit.
chmod +x /srv/job.sh. - No shebang. The first line should be
#!/bin/bashor#!/bin/sh. Bash syntax run byshfails on Debian, whereshis dash. - Windows line endings. A script saved with CRLF fails with
/bin/bash^M: bad interpreter. Fix it withsed -i 's/\r$//' job.sh. - Relative paths. Cron starts jobs in the user’s home directory, not next to the script.
cd /srv/app || exit 1at the top fixes it.
6. The crontab file itself
/etc/crontaband files in/etc/cron.d/have a user field:0 3 * * * root /srv/job.sh. Your personal crontab (crontab -e) doesn’t. Mixing them up breaks the line.- On Debian and Ubuntu, files in
/etc/cron.d/are ignored if their names contain a dot, and scripts in/etc/cron.daily/namedbackup.share skipped for the same reason. Usebackup. - A file installed from disk needs a newline after the last line, or that line may be ignored.
/etc/cron.allowand/etc/cron.denycan forbid a user to use cron.
7. The schedule isn’t what you think
- Time zone. Cron uses the server’s time zone, often UTC on cloud machines.
timedatectlshows it. After changing it, restart cron. - Day of month and day of week together. If both fields are set, the job runs when either matches:
0 0 1 * 1runs on the 1st and every Monday. - Steps.
*/45in the minute field runs at :00 and :45, not every 45 minutes.
Paste the expression into the cron expression explainer to see the next runs in plain words.
8. Errors go nowhere
Cron mails a job’s output to the user or to MAILTO. Without a mail server it’s lost. While debugging, write it to a file:
*/5 * * * * /srv/job.sh >> /var/log/job.log 2>&1
If nothing helps, add a temporary line * * * * * env > /tmp/cron-env, wait a minute and compare the file with env in your shell.
Know next time without checking
Every item above fails quietly. Instead of finding out weeks later, make the job report each run and get an alert when a report doesn’t come. Create a check in Heartcron with the same schedule and add a ping to the end of the line:
0 3 * * * /srv/backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://ping.heartcron.com/<uuid>
If cron stops, the server is rebuilt without the crontab, or the script fails, the ping doesn’t arrive and you get an alert in Telegram, Slack, Discord or email. To also see the error text, send the output and the exit code:
0 3 * * * /srv/backup.sh 2>&1 | tail -c 100000 | curl -fsS -m 10 --data-binary @- https://ping.heartcron.com/<uuid>/${PIPESTATUS[0]}
This one needs SHELL=/bin/bash at the top of the crontab, because PIPESTATUS is a Bash feature. Or install the open-source heartcron CLI and let it do the same: heartcron run nightly-backup -- /srv/backup.sh.