1. Verify the syntax
The expression might be valid syntax that means something different from what you intended. Paste it into the explainer to see the plain-English translation and next 5 run times. If the next run is "in 4 days" when you expected "every 5 minutes," the expression is wrong.
Common syntax mistakes:
0 9 * * 1-5intended as "every weekday at 9 AM" — correct* 9 * * 1-5means "every minute during the 9 AM hour, on weekdays" — probably not what you wanted*/5 * * * 1-5means "every 5 minutes, every weekday" — note this fires 288 times a day, not once
2. Confirm the crontab is actually loaded
If you edited a file but didn't crontab-import it, the system doesn't know. Verify:
crontab -l # Shows the active crontab for your user sudo crontab -u www-data -l # Check another user's crontab
If your job isn't in the output, install it: crontab my-cron-file. Editing /etc/crontab directly works on some systems but not all — prefer crontab -e per user.
3. Confirm the cron service is running
On systemd-based systems:
systemctl status cron # Debian/Ubuntu systemctl status crond # RHEL/CentOS/Fedora
If it's inactive, start it: sudo systemctl start cron && sudo systemctl enable cron.
Check the cron log for evidence your job actually ran:
grep CRON /var/log/syslog # Debian/Ubuntu journalctl -u crond # systemd
4. PATH and environment variables
Cron runs jobs with a minimal environment — typically just PATH=/usr/bin:/bin and a tiny set of variables. If your script uses python3, node, or anything in /usr/local/bin, it may not find them.
Two fixes:
- Set PATH at the top of your crontab:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - Use absolute paths in your script:
/usr/local/bin/python3instead ofpython3.
Same for other env vars: if your code reads $AWS_PROFILE or $DATABASE_URL, those won't exist in cron unless you set them explicitly or source a file with them.
5. Permissions on the script
The script must be executable by the user the cron job runs as:
ls -la /path/to/script.sh # If not executable: chmod +x /path/to/script.sh
For shell scripts, also confirm the shebang line is correct:
#!/usr/bin/env bash # portable #!/bin/bash # explicit
6. Output is being silently lost
By default, cron emails any output to the user's local mailbox. On most modern systems, there's no local mail setup, so the output just vanishes. Two important fixes:
- Capture output to a log file:
0 9 * * * /path/to/script.sh >> /var/log/my-job.log 2>&1
The2>&1redirects stderr to the same place as stdout, so error messages are visible too. - Set
MAILTOat the top of your crontab if you have local mail configured:MAILTO="your@email.com" 0 9 * * * /path/to/script.sh
7. Relative paths fail in cron
Cron starts each job in the user's home directory, not the directory where the script lives. A script that works manually:
./helper.sh # works from where you ran it config.json # works if you cd'd into the right dir
… will fail under cron because ./helper.sh and config.json are resolved relative to /home/user/. Fix it by:
- Using absolute paths everywhere:
/opt/myapp/helper.sh - Or
cd'ing first:cd /opt/myapp && ./helper.sh
8. Wrong timezone
Cron uses the system's local timezone, which on cloud servers is almost always UTC. So 0 9 * * 1-5 means 9 AM UTC, not 9 AM in your local time.
date # Shows the system timezone timedatectl # On systemd, shows + lets you set the TZ
To run in a specific timezone, either:
- Set the timezone in your script:
TZ='America/New_York' /path/to/script.sh - Set
CRON_TZ=America/New_Yorkat the top of your crontab (Vixie cron 4.0+) - Calculate the equivalent UTC time and use it in the cron expression
This is also where Daylight Saving Time bugs live — see our DST guide.
9. The shell is different
Cron uses /bin/sh by default, which on most Linux systems is dash, not bash. Constructs like [[ ... ]], arrays, and $'' string literals don't work in dash.
Fixes:
- Set
SHELL=/bin/bashat the top of your crontab - Or put a proper shebang at the top of your script:
#!/usr/bin/env bash
10. The day-field gotcha
If you've set both day-of-month and day-of-week (neither is *), Unix cron fires when EITHER matches — OR semantics. So 0 0 15 * 1 fires on the 15th of every month AND every Monday, not "the 15th if it falls on a Monday."
To get AND semantics, leave one day field as * and gate inside your script:
# Run only on the 15th if it's a weekday 0 0 15 * * [ $(date +\%u) -le 5 ] && /path/to/script.sh
Bonus: a debugging cron job
If a job won't run and you don't know why, install a tiny test job to confirm cron is processing your crontab at all:
* * * * * echo "hi from cron at $(date)" >> /tmp/cron-test.log
Wait two minutes. If /tmp/cron-test.log has entries, cron is working — the problem is in your real job. If the file is empty or doesn't exist, cron itself isn't running your crontab.