Cron has been scheduling Unix jobs since 1975. The syntax fits on a napkin. And yet, cron jobs are responsible for a disproportionate share of production incidents — because the failure modes are subtle, silent, and almost always discovered at the worst possible time.
Here are the ten mistakes that catch people most often, and how to prevent each one.
Paste any cron expression to see exactly when it will run. Catches syntax mistakes before they reach production.
1. The PATH isn't what you think it is
When you run a script from your terminal, your shell has a rich PATH variable — /usr/local/bin, /usr/bin, your language version managers, ~/.local/bin, and whatever else you've accumulated.
Cron doesn't use your shell's PATH. It uses a minimal default, typically just /usr/bin:/bin. So this works in your terminal:
python3 /home/deploy/scripts/cleanup.pyBut in cron, python3 isn't found because /usr/local/bin isn't in the PATH.
Fix: Always use absolute paths in crontab entries. Not just for the script — for every binary the script calls:
0 3 * * * /usr/local/bin/python3 /home/deploy/scripts/cleanup.pyOr set the PATH explicitly at the top of your crontab:
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * * python3 /home/deploy/scripts/cleanup.py2. Timezone confusion
Your server runs in UTC. Your crontab says 0 9 * * 1-5. You expect the job to run at 9am London time. It runs at 9am UTC — which is 10am BST in summer and 9am GMT in winter.
This bites hardest with daylight saving transitions. A job scheduled for 1:30am in a DST-observing timezone will either run twice or not at all on the transition days.
Fix: Know your server's timezone (timedatectl on Linux). If you need a specific local timezone, either set TZ=Europe/London in the crontab or use systemd timers which have first-class timezone support.
For cloud platforms, the timezone situation varies. AWS EventBridge Scheduler supports timezone-aware schedules. GitHub Actions runs in UTC only. Kubernetes CronJobs support .spec.timeZone since v1.25.
3. No output capture
By default, cron emails the output of every job to the crontab owner's local mailbox. In practice, nobody reads local mail on a server, so output vanishes silently. If the job fails, you'll never know.
Fix: Redirect both stdout and stderr to a log file:
0 3 * * * /path/to/script.sh >> /var/log/myjob.log 2>&1The 2>&1 ensures error messages go to the same file as normal output. Without it, errors vanish even when stdout is redirected.
Better yet, send output to a centralised logging service or write structured logs that your monitoring stack can alert on.
4. Overlapping executions
Your job runs every 5 minutes and usually takes 2 minutes. One day the database is slow and the job takes 7 minutes. Now two instances are running simultaneously — and possibly corrupting each other's data.
This is especially dangerous for jobs that modify shared state: processing a queue, updating a cache, writing to a file.
Fix: Use file locking. On Linux, flock is built-in:
*/5 * * * * flock -n /tmp/myjob.lock /path/to/script.shThe -n flag means "don't wait" — if the lock is held, the new invocation exits immediately rather than queuing up. No overlapping, no data corruption.
5. The "every minute" misunderstanding
New users sometimes write * * * * * thinking it means "run once" or "run at midnight." It means "run every minute, every hour, every day." That's 1,440 executions per day.
Similarly, 0 0 * * 0,6 doesn't mean "weekends at midnight" — it means "midnight on Saturday AND midnight on Sunday," which is correct but catches people who read 0,6 as "Saturday through Sunday" rather than "Sunday and Saturday."
Fix: Always verify your expressions before deploying. The cron explainer shows you exactly what an expression means in plain English, plus the next five scheduled runs. A 10-second check prevents a lot of pain.
6. Environment variables aren't loaded
Your script works perfectly when you run it as the deploy user:
su - deploy
/home/deploy/scripts/backup.sh # Works!But it fails from cron because cron doesn't source .bashrc, .profile, .env, or any other shell initialisation file. Environment variables like DATABASE_URL, AWS_ACCESS_KEY_ID, or NODE_ENV are simply not set.
Fix: Either source the environment file explicitly in the crontab entry:
0 3 * * * . /home/deploy/.env && /home/deploy/scripts/backup.shOr define the variables directly in the crontab:
DATABASE_URL=postgres://localhost:5432/mydb
0 3 * * * /home/deploy/scripts/backup.shOr, better yet, have the script itself load its environment from a file as its first action, rather than relying on the execution context.
7. Running as the wrong user
Crontab entries run as the user who owns the crontab. If you edited the crontab as root, the job runs as root. If you edited it as deploy, it runs as deploy.
This matters for file permissions, database access, SSH keys, and any operation that's user-specific. A backup script that works as root but fails as the app user is a common scenario.
Fix: Always check which user's crontab you're editing. crontab -e edits the current user's crontab. sudo crontab -e edits root's. System-wide crontabs in /etc/cron.d/ have a user field:
# /etc/cron.d/backup
0 3 * * * deploy /home/deploy/scripts/backup.sh8. Day-of-month AND day-of-week: OR, not AND
This is the most counterintuitive aspect of cron syntax. When both the day-of-month and day-of-week fields are specified (neither is *), they're combined with OR, not AND.
0 9 15 * 1You might read this as "9am on the 15th if it's a Monday." It actually means "9am on the 15th of every month, AND 9am on every Monday." The job runs far more often than intended.
Fix: If you need "9am on the 15th, but only if it's a weekday," you can't express it in cron alone. Use application-level logic: schedule for the 15th and check the day of the week in the script.
9. Not testing the expression before deploying
Developers who would never deploy code without tests routinely deploy cron expressions without verifying them. A misplaced * or a wrong number in the hour field can cause a job to run thousands of times or not at all.
# Intended: every day at 3am
0 3 * * *
# Actual mistake: every minute between 3:00 and 3:59
* 3 * * *That's a 60x difference in execution frequency from changing a single character.
Fix: Validate every expression before deploying. Paste it into the cron explainer and check the "Next 5 runs" section. If the runs don't match your expectations, fix the expression before it hits production.
10. No monitoring for non-execution
Most monitoring catches failures — the job ran and threw an error. Far fewer teams monitor for non-execution — the job simply didn't run at all.
Non-execution happens when:
- Someone deletes or comments out the crontab entry accidentally
- The cron daemon itself crashes or isn't running (
systemctl status cron) - The server was rebooted and cron didn't start automatically
- A syntax error in one crontab line silently breaks all lines below it
- The user who owns the crontab was deleted
Fix: Implement a dead man's switch. Have the job report to an external service on success (Cronitor, Healthchecks.io, or even a simple HTTP endpoint you monitor). If the expected check-in doesn't arrive within the expected window, you get alerted.
The simplest version is a curl at the end of your script:
#!/bin/bash
# ... do the actual work ...
# Report success
curl -fsS --retry 3 https://hc-ping.com/your-uuid-here > /dev/nullIf the script fails before reaching the curl, or if cron never runs it, the ping never arrives and you get notified.
The checklist
Before deploying any cron job, verify:
- [ ] Expression validated with the cron explainer — next runs match expectations
- [ ] Absolute paths for all binaries and scripts
- [ ] Environment variables loaded explicitly
- [ ] Output redirected to a log file (both stdout and stderr)
- [ ] File locking to prevent overlapping executions
- [ ] Running as the correct user
- [ ] Timezone documented and accounted for
- [ ] Dead man's switch or external monitoring configured
- [ ] Alert set for both job failures and non-execution