Skip to content
LastPing
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.

Your assistant can do this for you

Monitor my cron jobs with LastPing

On this page
  1. The 7 most common reasons a cron job stops running
  2. The permanent fix: dead-man's-switch detection
  3. Questions people ask

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: check with systemctl status cron; (2) wrong timezone: cron uses the system timezone or the CRON_TZ variable, not UTC unless your server is set to UTC; (3) PATH is too short: cron runs with a minimal environment, so commands not in /usr/bin:/bin are invisible, and commands that work in a shell often fail silently in cron because /usr/local/bin or ~/bin is not on PATH; (4) crontab syntax error: run crontab -l and verify with crontab.guru; (5) the script exits non-zero, errors go to an unread mail spool and nobody notices: add || curl .../fail to alert immediately; (6) the script exists but is not executable: check with ls -l and add chmod +x; (7) the job is in a different user's crontab: check sudo crontab -l as well as your own. 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 like username CMD (command). No line at all means the schedule didn't fire: check the cron daemon status and your crontab syntax. If you see the line but the job still seems broken, the job started and the problem is inside the script.

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

    Run the exact command cron would run, with cron's minimal environment: env -i HOME=$HOME /bin/sh -c 'your-command'. The env -i clears all environment variables, reproducing exactly what cron sees. Any "command not found" errors will surface immediately, without needing to wait for the next schedule window. This is the fastest way to catch PATH and environment issues without modifying the schedule.

  • How do I know if my cron job ran successfully or just silently failed?

    Add a ping to your crontab: 0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://ping.lastping.dev/<your-monitor-id>. The && means the ping only fires on success. If the job fails, the ping is skipped, and LastPing opens an incident when the expected signal doesn't arrive in the grace window. Add || curl .../fail for an immediate alert instead of waiting.

  • What does cron's minimal environment look like?

    Cron sets only HOME, LOGNAME, USER, SHELL (/bin/sh), and PATH (/usr/bin:/bin). Everything else (NVM paths, rbenv shims, conda envs, custom bin dirs) is absent. Solve this by (1) using full absolute paths in your command, (2) sourcing your profile explicitly: /bin/bash -l -c 'your-command', or (3) setting environment variables at the top of the crontab with VAR=value lines before any job entries.

  • 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 for individuals, fully hosted, and catches all of them. See dead-man's-switch explained for the full concept.

  • Is monitoring free?

    Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.

FREE FOR INDIVIDUALS · FULLY HOSTED

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.