You added a cron job, waited for it to fire, and nothing happened. No output, no error, no log entry — just silence. This is one of the most frustrating debugging situations in Linux because cron gives you almost no feedback by default.
The good news: there are a fixed number of things that can cause a cron job to not run, and you can check each one in about ten minutes. This guide walks through them in order of how often they come up.
chmod +x was never runHOME, DATABASE_URL, or other vars that cron doesn't provideBefore debugging the job itself, verify the scheduler is alive:
# Linux (systemd)
systemctl status cron # Ubuntu/Debian
systemctl status crond # RHEL/CentOS/Amazon Linux
# Linux (non-systemd)
service cron status
# macOS
launchctl list | grep cron
If it's stopped:
sudo systemctl start cron # Ubuntu/Debian
sudo systemctl start crond # RHEL/CentOS
On managed cloud platforms (Heroku Scheduler, GitHub Actions, AWS EventBridge) this isn't applicable — the scheduler is outside your control. Jump to step 3.
Cron logs tell you whether the daemon even attempted to run your job:
# Ubuntu/Debian (journald)
journalctl -u cron --since "1 hour ago"
# Ubuntu/Debian (syslog)
grep CRON /var/log/syslog | tail -50
# RHEL/CentOS
grep cron /var/log/cron | tail -50
# macOS
log show --predicate 'subsystem == "com.apple.xpc.launchd"' --last 1h | grep cron
What you're looking for:
CMD (/path/to/script.sh) — the daemon ran it; the problem is in the script itselfRELOAD or ERROR — crontab reload failed, often due to a syntax errorA single syntax error in crontab -e can silently disable the entire crontab on some distributions. Always edit with crontab -e rather than editing the file directly — it validates before saving.
# View your current crontab
crontab -l
# Edit safely (validates before saving)
crontab -e
Common syntax mistakes:
# Wrong: missing fields
*/5 * * /path/to/script.sh # only 3 fields before command — needs 5
# Wrong: space inside a field
0 9 * * 1-5 /path/to/script.sh # double space — usually fine, but check
# Wrong: percent sign in command (unescaped)
0 9 * * * echo "today: $(date +%Y-%m-%d)" # %Y, %m, %d — percent signs must be escaped as \%
0 9 * * * echo "today: $(date +\%Y-\%m-\%d)" # correct
% is treated as a newline. Commands using date +%Y-%m-%d, Python format strings, or printf with %s will silently fail or produce unexpected output unless every % is escaped as \%.
Use an expression parser if you're unsure about the schedule fields: validate your cron expression here to see the plain-English schedule and next run times.
This is the most common cause of "runs fine from terminal, does nothing in cron." Cron runs with a minimal PATH:
/usr/bin:/bin
Your interactive shell has a much richer PATH from .bashrc, .profile, or .zshrc. Commands like node, python3, npm, php, composer, aws, or anything installed to /usr/local/bin or /home/user/.local/bin are not found.
Add a PATH-dump job temporarily:
* * * * * env > /tmp/cron-env.txt
Wait a minute, then check what PATH cron actually has:
grep PATH /tmp/cron-env.txt
Option 1: Set PATH at the top of your crontab (applies to all jobs in the file):
PATH=/usr/local/bin:/usr/bin:/bin:/home/youruser/.local/bin
0 9 * * * /path/to/script.sh
Option 2: Use absolute paths in your script:
# Instead of:
node index.js
# Use:
/usr/local/bin/node /home/youruser/myapp/index.js
Option 3: Source your profile inside the script:
#!/bin/bash
source /home/youruser/.bashrc
# rest of your script
Two quick checks:
# Is the script executable?
ls -la /path/to/script.sh
# Fix if not:
chmod +x /path/to/script.sh
Also verify the shebang line is correct and points to an existing interpreter:
head -1 /path/to/script.sh
# Should be: #!/bin/bash or #!/usr/bin/env python3 etc.
# Verify the interpreter exists:
which bash
which python3
\r\n line endings. The shebang line #!/bin/bash\r will fail with bad interpreter. Fix with: sed -i 's/\r$//' script.sh
Cron sets the working directory to your home directory (~), not the directory where your script lives. Any relative path inside the script — ./config.json, ../data/, logs/app.log — will resolve relative to home, not relative to the script.
The fix: cd to your project directory at the top of your script:
#!/bin/bash
set -e
cd /home/youruser/myapp # absolute path to your project
# Now relative paths work as expected
./run.sh
python3 process.py
node dist/index.js
Or do it inline in the crontab:
0 9 * * * cd /home/youruser/myapp && ./run.sh
Cron doesn't load your .bashrc, .profile, or any shell startup file. Environment variables your script depends on — DATABASE_URL, API_KEY, HOME, LANG — won't be set unless you set them explicitly.
Option 1: Set them in the crontab file:
DATABASE_URL=postgres://user:pass@localhost/mydb
API_KEY=sk-abc123
0 9 * * * /path/to/script.sh
Option 2: Load a .env file inside your script:
#!/bin/bash
set -a
source /home/youruser/myapp/.env
set +a
# rest of your script
Option 3: Use a systemd timer instead of cron — systemd's EnvironmentFile= directive gives you a clean way to inject env vars into scheduled jobs:
# /etc/systemd/system/myjob.service
[Service]
Type=oneshot
EnvironmentFile=/home/youruser/myapp/.env
ExecStart=/home/youruser/myapp/script.sh
Cron has separate crontab files per user. If your job needs to run as root (to access certain files or ports), it won't appear in crontab -l as a regular user:
# Your own crontab
crontab -l
# Root's crontab
sudo crontab -l
# Another user's crontab (as root)
sudo crontab -l -u www-data
Also check system-wide cron directories — jobs placed here by package managers or sysadmins run on their own schedules:
ls /etc/cron.d/
ls /etc/cron.daily/
ls /etc/cron.hourly/
If you're not sure where to start, run through this in order:
sudo -u cronuser /path/to/script.sh. Does it succeed?* * * * * env > /tmp/cron-env.txt temporarily — confirm PATH and env vars.env -i HOME=/home/youruser PATH=/usr/bin:/bin /path/to/script.sh0 9 * * * /path/to/script.sh >> /tmp/myjob.log 2>&1Step 5 — redirecting both stdout and stderr to a log file — is the single most useful debugging tool. If cron is attempting your job but the job is failing silently, that log file will show you exactly what went wrong.
Debugging a failed cron job after the fact is slow and stressful. The better long-term solution is monitoring that alerts you the moment a job misses its window, before a user or customer notices first.
Once your job is running correctly, add a dead man's switch so you'll know the moment it stops — without grepping logs or waiting for a complaint. CronWatch monitors crontab, GitHub Actions scheduled workflows, Heroku Scheduler, and AWS EventBridge from a single dashboard.
Join the CronWatch waitlist → Validate your cron expressionNot sure whether your schedule expression is correct in the first place? Use the free cron expression parser to see the plain-English description and the next five run times before you deploy the job.