Cron Job Works Manually But Not in Crontab — How to Fix
Your script runs perfectly from the terminal. But cron refuses to cooperate. Here's why, and how to fix every possible cause.
Quick Diagnostic Checklist
which python3
head -1 script.sh
ls -la script.sh
* * * * * /path/to/script.sh >> /tmp/cron.log 2>&1
This is one of the most frustrating cron problems: your script works perfectly when you run it from the terminal, but silently does nothing when cron tries to execute it. The root cause is almost always a difference between your interactive shell environment and cron's minimal execution environment.
Let's walk through every cause, from most common to least, with the exact steps to diagnose and fix each one.
1. PATH Differences — The #1 Cause
When you open a terminal, your shell loads a rich PATH variable that includes directories like /usr/local/bin, /home/user/.local/bin, and wherever your language runtimes live. Cron does not. Cron's default PATH is typically just:
/usr/bin:/bin
So if your script calls python3, node, pip, docker, or any command installed outside those two directories, cron simply cannot find it.
Diagnose it: Check what PATH cron actually has by adding a temporary cron job:
* * * * * env > /tmp/cron-env.txt
Wait a minute, then check /tmp/cron-env.txt and compare the PATH line to your terminal's:
echo $PATH
Fix it: Use absolute paths for every command in your crontab or script:
# Bad — cron can't find python3
*/5 * * * * python3 /home/user/scripts/process.py
# Good — absolute path to python3
*/5 * * * * /usr/bin/python3 /home/user/scripts/process.py
Find the absolute path to any command with which:
which python3 # /usr/bin/python3
which node # /usr/local/bin/node
which docker # /usr/bin/docker
Alternatively, set PATH at the top of your crontab file:
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
*/5 * * * * python3 /home/user/scripts/process.py
2. Environment Variables Not Loaded
Your terminal session loads ~/.bashrc, ~/.bash_profile, ~/.profile, and possibly ~/.zshrc. These files often set critical variables like DATABASE_URL, API_KEY, HOME, LANG, and others. Cron loads none of them.
If your script depends on any environment variable, it will fail under cron — often silently, since the error output goes nowhere by default.
Fix option 1: Define variables directly in the crontab:
DATABASE_URL=postgres://localhost:5432/mydb
API_KEY=sk_live_abc123
LANG=en_US.UTF-8
0 * * * * /home/user/scripts/sync.sh
Fix option 2: Source an environment file in your script:
#!/bin/bash
source /home/user/.env
# or
source /home/user/.bashrc
# rest of your script
/usr/bin/python3 /home/user/app/main.py
Fix option 3: Source the profile inline in the crontab entry:
0 * * * * . /home/user/.profile; /home/user/scripts/backup.sh
Fixed it? Make sure it stays fixed — get alerted if this job stops running.
Sign up with Google3 monitors free. No credit card.
3. Shell Differences (sh vs bash)
Cron uses /bin/sh by default. On Debian and Ubuntu, /bin/sh is actually dash — a minimal POSIX shell that does not support bash-specific features like:
[[ double brackets ]]for conditionals- Arrays:
arr=(one two three) - Process substitution:
<(command) sourcecommand (use.instead in sh)- String manipulation:
${var//pattern/replace}
If your script works in bash but uses any of these features, it will fail under cron's default /bin/sh.
Fix option 1: Add the correct shebang to your script:
#!/bin/bash
# Now this script will always use bash, regardless of how cron invokes it
Fix option 2: Set the shell in your crontab:
SHELL=/bin/bash
*/5 * * * * /home/user/scripts/process.sh
4. Relative Paths in Your Script
When you run a script from the terminal, the working directory is wherever you are. When cron runs it, the working directory is typically the user's home directory — or sometimes /. Any relative file paths in your script will resolve differently.
Diagnose it: Add this to the top of your script to log the working directory:
echo "Working directory: $(pwd)" >> /tmp/cron-debug.log
Fix it: Use absolute paths for all file operations, or cd to the correct directory at the start of your script:
#!/bin/bash
cd /home/user/myproject || exit 1
# Now relative paths work as expected
./run.sh
cat config/settings.yaml
5. Missing Shebang Line
If your script doesn't have a shebang (#!) on the first line, the system uses /bin/sh to execute it. This can cause bash scripts, Python scripts, or any non-sh script to fail.
Check it:
head -1 /path/to/your/script.sh
If the first line is not a shebang, add one:
#!/bin/bash # for bash scripts
#!/usr/bin/python3 # for Python scripts
#!/usr/bin/env node # for Node.js scripts
Note: #!/usr/bin/env relies on PATH, which may not work in cron. Use the absolute path to the interpreter when possible:
#!/usr/bin/python3
6. Permission Issues
Your script must be executable. This is easy to forget, especially after copying files or pulling from git.
# Check permissions
ls -la /path/to/script.sh
# Make it executable
chmod +x /path/to/script.sh
Also verify that the cron user has read access to every file and directory your script touches. A script that works under your user account may fail if the crontab belongs to a different user (like root or www-data).
7. Output Redirection — See What's Actually Happening
By default, cron sends output to the user's local mailbox, which on most modern servers is never checked. This means errors vanish silently. Always redirect output to a log file when debugging:
*/5 * * * * /home/user/scripts/backup.sh >> /tmp/cron-backup.log 2>&1
The 2>&1 part captures both standard output and errors. Check the log after the job should have run to see exactly what went wrong.
The Definitive Test: Simulate Cron's Environment
If you've checked everything above and the job still fails, simulate cron's minimal environment to reproduce the issue locally:
env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME=$HOME /bin/sh -c '/path/to/your/script.sh'
This strips away all your shell's environment variables and PATH, running the script exactly as cron would. If it fails here, you've reproduced the bug and can fix it.
Even After Fixing, Cron Jobs Can Silently Fail Again
You've fixed the issue today. But cron jobs break again — a system update changes PATH, a new dependency gets added, a file permission changes, a disk fills up. And cron won't tell you when it happens.
The real fix is monitoring. Add a heartbeat ping to your cron job that only fires on success:
0 2 * * * /home/user/scripts/backup.sh && curl -fsS https://api.cronsignal.io/ping/your-check-id
If the job doesn't run — or runs but fails — the ping never fires, and CronSignal alerts you immediately via email, Slack, or webhook.
Related Guides
Related Resources
- Cron Environment Variables — Fix missing environment variables in cron
- Cron Command Not Found — Fix PATH issues in crontab
- Cron Expression Validator — Verify your cron expression syntax
- How to Monitor Cron Jobs — Set up cron job monitoring
Never debug silent cron failures again
CronSignal alerts you the moment a job doesn't run. Know about failures before they become disasters.
Sign up with Google3 monitors free. No credit card required.
Related Troubleshooting Guides
Need help with cron expressions? Use our cron validator to check syntax or cron generator to build expressions visually.