Cron is the time-based job scheduler in Unix-like systems. It has been a cornerstone of Linux administration since the 1970s, and it remains the go-to solution for running recurring tasks โ from log rotation and database backups to cache purges and monitoring checks. Despite its simplicity, cron has subtle behaviors that trip up even experienced engineers.
This guide covers everything from the five-field expression format to production-grade best practices that prevent the silent failures that make cron jobs notorious.
The Five-Field Cron Expression
A cron expression consists of five fields that define when a job runs. Each field represents a unit of time:
โโโโโโโโโโโโโโ minute (0โ59)
โ โโโโโโโโโโโโ hour (0โ23)
โ โ โโโโโโโโโโ day of month (1โ31)
โ โ โ โโโโโโโโ month (1โ12)
โ โ โ โ โโโโโโ day of week (0โ7, 0 and 7 = Sunday)
โ โ โ โ โ
* * * * * command-to-execute
Special characters include * (any value),, (list),- (range), and/ (step). For example,*/15 means "every 15 units."
Practical Examples
# Every day at 2:30 AM
30 2 * * * /opt/scripts/backup.sh
# Every 15 minutes
*/15 * * * * /usr/bin/health-check.sh
# Weekdays at 9 AM
0 9 * * 1-5 /opt/scripts/daily-report.sh
# First day of every month at midnight
0 0 1 * * /opt/scripts/monthly-cleanup.sh
# Every Sunday at 4 AM
0 4 * * 0 /opt/scripts/weekly-maintenance.sh
Managing Crontabs
Each user has their own crontab. The system also has crontab files in/etc/cron.d/ and time-based directories like/etc/cron.daily/.
$ crontab -e ย ย ย # edit your crontab
$ crontab -l ย ย ย # list your crontab
$ crontab -r ย ย ย # remove your crontab (careful!)
$ sudo crontab -u www-data -e # edit another user's crontab
For system-wide jobs, placing scripts in /etc/cron.d/ with a username field is often cleaner than editing root's crontab directly. These files survive package upgrades and are easier to manage with configuration management tools like Ansible.
The Environment Problem
The number one reason cron jobs fail silently: cron runs with a minimal environment. Your interactive shell has PATH, environment variables, and shell configurations loaded from.bashrc or.profile. Cron does not load any of these.
# This will probably fail in cron:
* * * * * node /opt/app/script.js
# This will work โ use absolute paths:
* * * * * /usr/bin/node /opt/app/script.js
# Or set PATH at the top of your crontab:
PATH=/usr/local/bin:/usr/bin:/bin
Always use absolute paths for both the command and any files it references. If your script depends on environment variables, define them at the top of the crontab or source a config file at the beginning of your script.
Output and Logging
By default, cron sends any output (stdout and stderr) to the user's local mailbox. On most modern servers, local mail is not configured, so this output is silently lost. Always redirect output explicitly:
# Log both stdout and stderr
30 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Log with timestamps
30 2 * * * /opt/scripts/backup.sh 2>&1 | /usr/bin/ts '[%Y-%m-%d %H:%M:%S]' >> /var/log/backup.log
# Discard output (only if you truly don't care)
*/5 * * * * /opt/scripts/heartbeat.sh > /dev/null 2>&1
Never redirect to /dev/null unless you have separate monitoring in place. Silent failures in cron jobs can go unnoticed for weeks or months.
Preventing Overlapping Runs
If a cron job takes longer than its interval, you can end up with multiple copies running simultaneously. This is dangerous for jobs that modify databases, move files, or hold locks. Useflock to prevent overlap:
# Use flock to ensure only one instance runs
*/5 * * * * /usr/bin/flock -n /tmp/myapp.lock /opt/scripts/process-queue.sh
# -n means non-blocking: skip if lock is held
# -w 60 means wait up to 60 seconds for the lock
For critical jobs, combine flock with alerting: if the lock is held for longer than expected, your monitoring should flag it.
Cron vs. Systemd Timers
Modern Linux distributions offer systemd timers as an alternative to cron. Here is when each makes sense:
| Feature | Cron | Systemd Timer |
|---|---|---|
| Setup complexity | Simple (one line) | Two files (.service + .timer) |
| Logging | Manual redirect | journalctl built-in |
| Dependencies | None | After=network.target etc. |
| Overlap prevention | Manual (flock) | Built-in |
| Portability | All Unix systems | systemd only |
Use cron for simple, portable tasks. Use systemd timers when you need dependency ordering, built-in logging, resource controls (CPU/memory limits), or accurate sub-minute scheduling.
Production Best Practices
1. Always log output โ redirect both stdout and stderr to a file with timestamps.
2. Use absolute paths โ for commands, scripts, and files. Never rely on PATH.
3. Use flock for long-running jobs โ prevent dangerous overlapping executions.
4. Set MAILTO โ even if you redirect output, set MAILTO to catch unexpected errors.
5. Test with the cron environment โ run env -i /bin/sh -c 'your-command' to simulate cron's minimal environment.
6. Monitor job execution โ use a dead man's switch service or check logs to verify jobs ran.
7. Version control your crontabs โ store cron configs in git alongside your application code.
8. Stagger job times โ avoid scheduling everything at midnight to prevent resource spikes.