Cron job not running: how to find out why
Cron fails quietly by design. Here is the order to check things in, from the five causes that account for almost every case.
Cron does not tell you when a job fails, and it does not tell you when a job never started. It was written for an era when the machine had a local mail spool and somebody read it. The result is that a broken cron job looks exactly like a working one until somebody notices the consequences.
Work through these in order. The first three cover almost every case.
1. Did cron try to run it?
This is the fork in the road: if cron started the job, you have a job problem; if it did not, you have a cron problem. Cron logs every launch it attempts.
# Debian, Ubuntu
journalctl -u cron --since "2 days ago" | grep your-script
grep CRON /var/log/syslog
# RHEL, Fedora, Amazon Linux
journalctl -u crond --since "2 days ago"
grep CROND /var/log/cron
A line naming your command means cron ran it and the problem is inside the job. No line at all means cron never started it, and the next two sections apply.
Note what this log does not contain: the job's own output, or its exit code. Cron records the launch, not the outcome.
2. Are you looking at the crontab that is actually installed?
There are at least four places a schedule can live, and editing the wrong one is a common way to lose an afternoon:
crontab -l— the current user's crontab. Jobs installed byrootare invisible here unless you runsudo crontab -l./etc/crontaband/etc/cron.d/*— system crontabs. These have an extra field: the user to run as, between the schedule and the command. A line copied from a user crontab into/etc/cron.dwithout that field silently never runs./etc/cron.{hourly,daily,weekly}— scripts run by run-parts, which ignores files with a dot in the name. A script calledbackup.shincron.dailyis never executed. Rename it tobackup.- systemd timers —
systemctl list-timers. On many modern distributions the thing you think is cron is a timer, and it has its own reasons for not firing.
Two file-level traps: a crontab whose last line has no trailing newline may have that line ignored, and a % anywhere in the command is turned into a newline unless you escape it as \%. That one bites every date +%Y-%m-%d in a crontab.
3. Does the job work in cron's environment?
Cron runs your command with almost nothing set. No .bashrc, no .profile, a PATH of roughly /usr/bin:/bin, SHELL set to /bin/sh rather than bash, and the user's home as the working directory. A script that works in your terminal and fails under cron is nearly always this.
Reproduce it rather than guessing:
env -i /bin/sh -c 'cd /; /path/to/your/script.sh'
The fix is to be explicit. Use absolute paths for the interpreter and the script, activate the virtualenv by calling its interpreter directly, and cd to the working directory the script expects:
0 2 * * * cd /srv/app && /srv/app/.venv/bin/python /srv/app/export.py
4. It ran, and it failed
Cron mails output to the crontab's owner, which on most cloud images goes nowhere at all. Capture it yourself:
0 2 * * * /srv/app/export.sh >> /var/log/export.log 2>&1
Do not use > /dev/null 2>&1 on a job you care about. It is the single most effective way to make a failure invisible.
5. The machine, the clock and the timezone
Cron fires in the system timezone, which on servers is usually UTC. Check with timedatectl. A daily job scheduled for 02:00 local time on a UTC machine runs at a different hour than you intended, and around a daylight-saving change a job scheduled in a DST-observing zone can run twice or not at all.
And if the machine was suspended or powered off when the job was due, plain cron simply skips it. anacron exists precisely to catch up jobs on machines that are not always on; a laptop or a VM that is stopped overnight needs it, or a timer with Persistent=true.
How to find out next time without looking
Everything above is diagnosis after the fact. The reason it took so long to notice is that no log can record the run that never happened. A job that was never started writes nothing, and a machine that was off writes nothing at all.
The reliable answer is to make the job report to something outside the machine on every success, and to be alerted when that report does not arrive on time. That is a dead man's switch, and it is one line:
0 2 * * * /srv/app/export.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://vivere.dev/p/<your-monitor-id>
Because of the &&, the ping only happens if the script exits zero. If the script fails, if cron never runs it, if the machine is off, or if the network is down, the ping does not arrive and you are told. Report the exit code instead of a bare success and you get the failure immediately rather than at the deadline:
0 2 * * * /srv/app/export.sh; curl -fsS -m 10 -o /dev/null https://vivere.dev/p/<id>/$?
Common questions
Why does my cron job work manually but not from crontab?
Cron runs with a nearly empty environment: no PATH beyond /usr/bin:/bin, no shell profile, and a different working directory. A command that relies on a PATH entry, a virtualenv, or a relative path works in your shell and fails under cron. Use absolute paths for both the interpreter and the script.
How do I see cron logs on Linux?
On Debian and Ubuntu, journalctl -u cron or /var/log/syslog filtered on CRON. On RHEL, Fedora and Amazon Linux, journalctl -u crond or /var/log/cron. These record that cron started the job, not what the job printed, so redirect the job's own output to a file if you want it.
How do I know a cron job did not run at all?
Nothing on the machine will tell you: cron logs a job it started and is silent about one it never started. The only reliable way is to have the job report to something outside the machine each time it succeeds, and to be alerted when that report does not arrive.
Find out without looking
Vivere watches for the ping that does not arrive. Add one line to the job, pick where alerts should land, and you hear about the run that never happened.
Start free Read the quickstart
Ten monitors, a status page, email and webhook alerts. No card.
Last reviewed September 2026.
Related
- How to know if your backup actually ran — Backups fail quietly. How to verify a backup ran, why exit code zero is not proof, what to check about size and restore, and how to be told the night it does not happen.
- A dead man's switch for your scripts — What a dead man's switch is, why it catches failures that error-based alerting cannot, how to build one with cron and curl, and the timing rules that stop it crying wolf.
- How to monitor a Python script that runs on a schedule — Monitoring a scheduled Python script: reporting success and failure with requests, catching exceptions with a decorator, capturing tracebacks, and alerting when a run never happens.