GUIDE · CRON · DIAGNOSIS

Why is my cron job not running?

A cron job that stops silently is the hardest failure to diagnose — because there's nothing to see. No error, no log entry, no email. Here are the seven most common causes, with the diagnostic command for each, and then the fix that makes sure you know immediately the next time it happens: one curl ping appended to your crontab line.

The 7 most common reasons a cron job stops running

Work through these in order — earlier ones are more common.

  • Cause 01

    The cron daemon isn't running

    Before anything else: is cron (or crond) actually running?

    # systemd-based systems (Ubuntu 16+, Debian, RHEL 7+) systemctl status cron # or: systemctl status crond # SysV init / older systems service cron status # If stopped: sudo systemctl start cron && sudo systemctl enable cron
  • Cause 02

    Wrong timezone — the job runs at the wrong time or not at all

    Cron uses the system timezone unless you set CRON_TZ at the top of the crontab. If your server is in UTC but you wrote the schedule thinking in local time (or vice versa), the job fires at an unexpected hour — or its window passes while you're watching the wrong clock.

    # Check the server's timezone timedatectl # Override per-job in crontab: CRON_TZ=America/New_York 0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://ping.lastping.dev/<your-monitor-id>
  • Cause 03

    Minimal PATH — commands not found silently

    Cron's default PATH is /usr/bin:/bin. Any binary in /usr/local/bin, ~/.nvm/versions/node/…/bin, ~/.rbenv/shims, or a conda environment is invisible. The job fails immediately with "command not found" — which cron captures in the local mail spool and nowhere else.

    # Reproduce cron's environment locally: env -i HOME=$HOME /bin/sh -c 'your-command' # Fix 1: use full absolute paths 0 3 * * * /usr/local/bin/python3 /home/deploy/backup.py # Fix 2: set PATH at the top of crontab PATH=/usr/local/bin:/usr/bin:/bin 0 3 * * * backup.py # Fix 3: source your profile (slower, but catches everything) 0 3 * * * /bin/bash -l -c '/usr/local/bin/backup.sh'
  • Cause 04

    Syntax error in the crontab

    A typo in the schedule expression — five fields, not four or six; a */ where only a number works; a tab/space issue — means the job is registered but never scheduled. Cron usually logs a parse error to syslog but not always visibly.

    # List your crontab — look for parse errors on load crontab -l # Validate online: https://crontab.guru # Example: "0 3 * * *" = 03:00 every day # Common mistake: "0 3 * * * *" — 6 fields, not 5; the 6th is treated as command
  • Cause 05

    Script exits non-zero but nobody is listening

    If your script returns a non-zero exit code, cron sends output to MAILTO — typically a local spool file that nobody reads. The job "ran" from cron's perspective; the failure was inside the script and went nowhere useful.

    # Detect in cron logs grep CRON /var/log/syslog | grep "(backup.sh)" # Add immediate failure alerting 0 3 * * * /usr/local/bin/backup.sh \ && curl -fsS -m 10 --retry 3 -o /dev/null https://ping.lastping.dev/<id> \ || curl -fsS -m 10 --retry 3 -o /dev/null https://ping.lastping.dev/<id>/fail
  • Cause 06

    Script not executable

    A script deployed without executable permission runs fine via bash script.sh but fails silently when cron tries to exec it directly. Check the permission bits.

    # Check ls -l /usr/local/bin/backup.sh # Fix chmod +x /usr/local/bin/backup.sh
  • Cause 07

    Wrong user's crontab

    The crontab was installed under a different user — often root or a deploy user. Check all relevant accounts. Also check /etc/cron.d/ and /etc/crontab, which include a username column that regular crontabs don't.

    # Check root's crontab sudo crontab -l # Check system crontabs cat /etc/crontab ls /etc/cron.d/ # See all running cron jobs (requires root) for user in $(cut -f1 -d: /etc/passwd); do sudo crontab -u "$user" -l 2>/dev/null | grep -v '^#' | sed "s/^/$user: /" done

The permanent fix: dead-man's-switch detection

Diagnosing the current problem is necessary. Making sure you know immediately about the next one is better. One curl ping appended to the crontab line does both.

Create a heartbeat monitor in LastPing

In the console, create a monitor with the job's schedule and a grace window. Copy the ping URL.

https://ping.lastping.dev/<your-monitor-id>

Append the ping (success only) to your crontab

The && means the ping only fires if the job exits 0. A failed or never-started job leaves silence — and LastPing opens an incident.

# m h dom mon dow command 0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://ping.lastping.dev/<your-monitor-id>

Optional: alert immediately on failure too

Add || curl …/fail to be paged the moment the job fails, instead of waiting for the grace window to expire.

0 3 * * * /usr/local/bin/backup.sh \ && curl -fsS -m 10 --retry 3 -o /dev/null https://ping.lastping.dev/<id> \ || curl -fsS -m 10 --retry 3 -o /dev/null https://ping.lastping.dev/<id>/fail

This approach catches all seven causes above: the daemon stopped (no ping), wrong timezone (ping arrives at the wrong time and triggers a late alert), script failure (ping skipped), and more. For the full copy-paste guide including hung-job detection and wrapper scripts, see Monitor cron jobs. Running on Kubernetes? See Kubernetes CronJobs. For the concept behind why absence is the reliable signal, see dead-man's-switch monitoring explained.


Questions people ask

Front-loaded answers — the most important fact first.

  • Why is my cron job not running?

    The seven most common causes: (1) cron daemon stopped; (2) wrong timezone; (3) PATH is too short — commands not in /usr/bin:/bin are invisible; (4) crontab syntax error; (5) script exits non-zero and errors go to an unread mail spool; (6) script not executable; (7) job is in a different user's crontab. Work through the checklist above in order.

  • How do I check if cron actually ran my job?

    Check the system log: grep CRON /var/log/syslog (Debian/Ubuntu) or grep cron /var/log/cron (RHEL/CentOS). Each run leaves a line with CMD (command). No line at all means the schedule didn't fire. A line with no follow-up means the job started and the problem is inside the script.

  • How do I test a cron job without waiting for the schedule?

    Run it with cron's minimal environment: env -i HOME=$HOME /bin/sh -c 'your-command'. The env -i clears all variables, reproducing exactly what cron sees. Any "command not found" errors will surface immediately, without needing to wait for the next schedule window.

  • What is dead-man's-switch monitoring for cron?

    Instead of the cron job reporting failures (which requires it to run), you set up an external expectation: "I expect a ping from this job every day at 03:00 UTC plus 30 minutes grace. If no ping arrives, open an incident." That expectation catches everything — daemon stopped, crontab lost, script hung, machine rebooted without the job — because none of those can report their own absence. LastPing is free, fully hosted, and catches all of them. See dead-man's-switch explained for the full concept.

  • Is monitoring free?

    Yes. LastPing is free and fully hosted with no monitor cap and no paid tier. See how it compares to Healthchecks.io and Cronitor.

FREE · FULLY HOSTED · NO PAID TIER

Fix the current problem. Prevent the next one.

One crontab edit puts detection in place permanently. You'll know the moment the next failure happens — not when a user reports it or a backup turns out to be missing.