// debugging

Cron Job Not Running?
10 Reasons Why (And Exactly How to Fix Each)

📅 March 2026 ⏱ 9 min read 🏷 Debugging · Linux · DevOps

It's 9am. Your backup didn't run. No error. No log entry. No warning. Just silence. Silent cron failures are one of the most infuriating experiences in Linux administration — the job doesn't run, and cron tells you absolutely nothing about why.

This guide covers all 10 causes, in order of likelihood, with the exact diagnostic commands and fixes for each. By the end you'll have a systematic process that solves 99% of cron failures in under 10 minutes.

// jump to the most likely cause
  1. Wrong expression syntax
  2. PATH is not your PATH
  3. Script is not executable
  4. Cron daemon is not running
  5. Relative paths in the script
  6. Wrong timezone on the server
  7. Output is swallowing errors
  8. Environment variables missing
  9. Overlapping job execution
  10. Wrong user or permission on the file
// 60-second first-pass diagnostic — run these first
1
Check if cron is running: systemctl status cron (Ubuntu) or systemctl status crond (RHEL/CentOS)
2
Check if your job appeared in logs at all: grep CRON /var/log/syslog | tail -30
3
Validate your expression: paste it into cronbuilder.dev and check the next 10 run times
4
Run the command manually as the cron user: sudo -u www-data /path/to/script.sh
01
Wrong Expression Syntax
Most common

Cron expressions are subtle. A single wrong character means the job never fires — and cron logs this as a parse error, not as a failure, so it's easy to miss.

Diagnose

# Look for "bad minute" or similar parse errors
grep CRON /var/log/syslog | grep -i "error\|bad\|invalid"

Common mistakes that look correct but aren't

# WRONG: space between */ and 5
* /5 * * * *

# WRONG: 6 fields on a standard Unix cron
0 */5 * * * *

# WRONG: value out of range (hour goes 0-23, not 1-24)
0 24 * * *

# WRONG: day-of-week 7 is non-standard (use 0 for Sunday)
0 9 * * 7

Fix

Paste your expression into CronBuilder.dev. If it's valid, you'll see the next 10 run times and a plain-English explanation. If it's invalid, you'll see the exact field that's wrong and why.

02
PATH Is Not Your PATH
Extremely common

This is the single most common cause of "works manually, fails in cron." When you run a command in your terminal, your shell loads .bashrc, .bash_profile, and your full PATH. Cron loads none of that. It starts with a bare-minimum PATH of approximately /usr/bin:/bin.

So when your script calls python3, node, npm, docker, php, or virtually any tool installed via a package manager — cron can't find it.

Diagnose

# See exactly what PATH cron will use
* * * * * env > /tmp/cron-env.log

# Then check the file
cat /tmp/cron-env.log | grep PATH

Fix — Option 1: Use absolute paths (safest)

# Find the full path of the binary
which python3       # → /usr/bin/python3
which node          # → /usr/local/bin/node  (or ~/.nvm/versions/...)

# Then use the absolute path in crontab
0 2 * * * /usr/bin/python3 /home/user/backup.py

Fix — Option 2: Set PATH at the top of your crontab

PATH=/usr/local/bin:/usr/bin:/bin:/home/user/.nvm/versions/node/v20.0.0/bin
MAILTO=""

0 2 * * * python3 /home/user/backup.py

NVM users: NVM sets up PATH in your .bashrc which cron never loads. Always use the full path like /home/user/.nvm/versions/node/v20.11.0/bin/node, or hardlink to /usr/local/bin/node.

03
Script Is Not Executable
Very common

If you call a script directly (e.g. /home/user/backup.sh), it must have the execute bit set for the user cron runs as. This is easy to miss when you create a new script or transfer files from another machine.

Diagnose

ls -la /home/user/backup.sh
# Look for the x bit: -rwxr-xr-x means executable
# -rw-r--r-- means NOT executable — this is the problem

Fix

# Make executable by the owner
chmod +x /home/user/backup.sh

# Or more precisely
chmod 755 /home/user/backup.sh

Alternatively, call it via the interpreter directly to sidestep permissions entirely:

# No executable bit needed when calling via interpreter
0 2 * * * /bin/bash /home/user/backup.sh
0 2 * * * /usr/bin/python3 /home/user/backup.py
04
Cron Daemon Is Not Running
Common after reboots

Cron requires a background daemon to be running at all times. After a server reboot, a crash, or an OS upgrade, the daemon may not have restarted.

Diagnose and fix

# Ubuntu / Debian
systemctl status cron
sudo systemctl start cron
sudo systemctl enable cron  # ensure it starts on boot

# RHEL / CentOS / Fedora
systemctl status crond
sudo systemctl start crond
sudo systemctl enable crond

# Older systems using SysV init
service cron status
service cron start
05
Relative Paths Inside the Script
Very common

Cron sets the working directory to the user's home directory, not to the directory where the script lives. Any relative path inside the script like ../data/input.csv or ./config.json will resolve to a completely wrong location.

Fix — Set working directory explicitly in the script

#!/bin/bash
# Always the first line of any cron-called script
cd "$(dirname "$0")" || exit 1

# Now relative paths resolve relative to the script's location
python3 process.py --input ./data/input.csv

Or set it in the crontab itself

# cd first, then run the script
0 2 * * * cd /home/user/myapp && ./run.sh
06
Timezone Mismatch
Especially common on cloud servers

Cloud servers are almost always set to UTC. If you write a cron expression thinking about your local time (e.g. "9am") but the server is UTC and you're in UTC+1, the job will fire at 8am your time — or worse, it appears not to run on the day you expect because the server's date rolled over at a different time to yours.

Diagnose

# Check what timezone the server thinks it is
date
timedatectl

Fix — Set timezone per crontab (modern cron supports this)

CRON_TZ=Europe/London
0 9 * * 1-5 /home/user/morning-report.sh

CRON_TZ=America/New_York
0 17 * * 1-5 /home/user/eod-export.sh

Or just always think in UTC

# 9am London = 8am UTC (winter), 9am London = 8am UTC (summer)
# Skip the mental maths — use CRON_TZ above

Tip: Use the CronBuilder timezone selector to preview exactly when your expression fires in any timezone — without any mental arithmetic.

07
Output Is Swallowing Your Errors
Easy to miss

By default, cron emails stdout/stderr to the system user — but most servers don't have a mail agent configured, so output silently disappears. If you've redirected stdout to /dev/null but not stderr, or vice versa, errors vanish completely.

Fix — Log both stdout and stderr to a file

# Redirect both stdout (1) and stderr (2) to a log file
0 2 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1

# Append with timestamp for easier reading
0 2 * * * echo "=== $(date) ===" >> /var/log/backup.log && /home/user/backup.sh >> /var/log/backup.log 2>&1

Disable email notifications if you're using file logging

# Add this at the top of your crontab
MAILTO=""

Common trap: > /dev/null 2>&1 discards all output and errors. Great for silencing noise, terrible for debugging. While you're debugging, always write to a log file instead.

08
Environment Variables Missing
Common with app scripts

Your script works manually because you've set environment variables in .bashrc, .bash_profile, or via direnv. Cron doesn't load any of these. API keys, database URLs, config paths — all missing.

Fix — Source a .env file inside the script

#!/bin/bash
# Load environment variables at the start of the script
set -a
source /home/user/myapp/.env
set +a

python3 myapp.py

Or set variables in the crontab itself

DATABASE_URL=postgresql://user:pass@localhost/mydb
API_KEY=your_key_here
MAILTO=""

0 2 * * * /home/user/myapp/backup.py

Diagnose exactly what environment cron sees

# This writes the full cron environment to a file
* * * * * env > /tmp/cron-environment.txt

# Then compare with your shell environment
env > /tmp/shell-environment.txt
diff /tmp/shell-environment.txt /tmp/cron-environment.txt
09
Overlapping Executions
Causes data corruption

If a job takes longer to complete than its interval, a second instance starts before the first one finishes. This doesn't prevent jobs from running — it causes too many to run. Side effects include database lock conflicts, corrupted output files, and resource exhaustion that eventually causes jobs to fail.

Diagnose

# Check if multiple instances are running right now
ps aux | grep backup.sh | grep -v grep

Fix — Use flock to prevent overlapping

# flock exits immediately if the lock is already held
0 */5 * * * flock -n /tmp/myapp.lock /home/user/myapp/process.sh

# Or with a timeout: wait up to 10 seconds for the lock
0 */5 * * * flock -w 10 /tmp/myapp.lock /home/user/myapp/process.sh
10
Wrong User or File Permissions
Common on multi-user servers

Your crontab runs as your user, but the script or the files it needs might be owned by a different user, or in a directory the cron user can't read. This is especially common when scripts are shared between users or when cron runs as www-data or another service account.

Diagnose

# Check who owns the script and what permissions it has
ls -la /path/to/script.sh

# Test running it as the cron user
sudo -u www-data /path/to/script.sh

Fix

# Change ownership to the user running cron
sudo chown your-user:your-user /path/to/script.sh

# Or set correct permissions
chmod 755 /path/to/script.sh

If you need cron to run as a different user, use the system crontab at /etc/crontab which has an extra username field:

# /etc/crontab — note the username field between schedule and command
0 2 * * * www-data /var/www/html/maintenance.sh

Start with the expression

90% of cron debugging sessions end at step one: the expression was wrong. Paste yours into CronBuilder.dev to eliminate that possibility immediately — see the next 10 run times, the exact field breakdown, and detailed error messages if something's off.

Validate My Expression →

The Definitive Debugging Checklist

Work through these in order. Most issues are solved by step 4.

□ 1. Is the cron daemon running?
      systemctl status cron

□ 2. Is the expression valid?
      Paste into cronbuilder.dev

□ 3. Did the job even get invoked?
      grep CRON /var/log/syslog | tail -30

□ 4. Does the command work manually as the cron user?
      sudo -u your-cron-user /path/to/script.sh

□ 5. Are you using absolute paths for all binaries?
      which python3 → use that full path

□ 6. Is the script executable?
      ls -la /path/to/script.sh → look for x bit

□ 7. Are relative paths inside the script resolving correctly?
      Add: cd "$(dirname "$0")" || exit 1

□ 8. Are you logging output and errors?
      Append: >> /var/log/myjob.log 2>&1

□ 9. Is the server timezone what you expect?
      timedatectl

□ 10. Are overlapping instances stacking up?
       ps aux | grep script-name.sh | grep -v grep

Related: Cron vs systemd timers — when to use each on Linux. GitHub Actions cron schedule — gotchas and UTC.