Cron is one of those tools that’s been around so long and is so widely used that most people learn just enough to make it work, then stop. But there are enough gotchas and useful features that it’s worth understanding properly.
The Cron Daemon and Config Files
crond (or cron on some systems) is the daemon that reads job schedules and executes them. Configuration lives in several places:
/etc/crontab— system-wide crontab (includes a user field)/etc/cron.d/— individual drop-in files (same format as /etc/crontab)/var/spool/cron/crontabs/username— per-user crontabs (edited withcrontab -e)/etc/cron.daily/,/etc/cron.hourly/,/etc/cron.weekly/,/etc/cron.monthly/— scripts that run at those intervals
Crontab Format
# m h dom mon dow command
* * * * * /usr/local/bin/myscript.sh
Five fields before the command:
- m — minute (0–59)
- h — hour (0–23)
- dom — day of month (1–31)
- mon — month (1–12 or names)
- dow — day of week (0–7, where both 0 and 7 are Sunday, or names)
Special values:
*— every value*/5— every 5 (step)1-5— range1,3,5— list
Some examples:
# Every minute
* * * * * /path/to/command
# Every 5 minutes
*/5 * * * * /path/to/command
# Daily at 2:30am
30 2 * * * /path/to/command
# Every Monday at 9am
0 9 * * 1 /path/to/command
# First day of each month at midnight
0 0 1 * * /path/to/command
# Weekdays at 8am
0 8 * * 1-5 /path/to/command
# Every 15 minutes between 9am-5pm on weekdays
*/15 9-17 * * 1-5 /path/to/command
Editing Your Crontab
crontab -e # Edit your crontab (opens $EDITOR)
crontab -l # List current crontab
crontab -r # Remove your crontab (careful — no confirmation)
For the system crontab or drop-ins, edit the files directly:
vim /etc/cron.d/myapp
The /etc/cron.d/ format includes a username field:
30 2 * * * root /usr/local/bin/backup.sh
The user field (root in this case) tells cron which user to run the command as.
The PATH Problem
This is the source of most cron problems. Cron runs with a minimal environment and a very limited PATH. Commands that work in your terminal fail in cron because the executable isn’t in cron’s PATH.
Solutions:
Use full paths:
0 3 * * * /usr/bin/python3 /home/deploy/scripts/report.py
Set PATH in the crontab:
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 3 * * * report.py
Source your profile in a wrapper script:
#!/bin/bash
source /etc/profile
source ~/.bashrc
/home/deploy/scripts/report.py
I use full paths by default. It’s explicit and eliminates ambiguity.
Handling Output
By default, cron emails output (stdout + stderr) to the user. This usually means it goes nowhere (root’s email isn’t read on most servers) or clogs your mail spool.
Redirect to a log file:
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
>> appends to the log file, 2>&1 redirects stderr to stdout (so both go to the log).
Or suppress all output:
0 3 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
I avoid suppressing output — if a job fails, you want to know why. Log to a file and set up log rotation.
For email notification of failures only, use a wrapper:
#!/bin/bash
output=$(/usr/local/bin/backup.sh 2>&1)
if [ $? -ne 0 ]; then
echo "$output" | mail -s "Backup failed" you@example.com
fi
Preventing Overlapping Runs
If a cron job takes longer than its interval, multiple instances can stack up. For jobs that shouldn’t overlap (backups, database maintenance), use flock:
0 * * * * flock -n /tmp/myapp.lock /usr/local/bin/myapp.sh >> /var/log/myapp.log 2>&1
flock -n acquires a lock or exits immediately if already locked. The -n means non-blocking — if the lock is held, the new instance exits rather than waiting.
Or use a PID file approach:
#!/bin/bash
PIDFILE=/tmp/myapp.pid
if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
echo "Already running"
exit 1
fi
echo $$ > "$PIDFILE"
trap "rm -f $PIDFILE" EXIT
# ... your script here ...
Environment Variables in Crontab
Set variables at the top of the crontab:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=""
0 3 * * * /usr/local/bin/backup.sh
MAILTO="" disables email output. SHELL=/bin/bash ensures bash syntax works (default cron shell varies by system).
Testing Cron Jobs
The most reliable test is to run the command manually as the correct user:
# Test as yourself
/usr/local/bin/backup.sh
# Test as root (if the cron runs as root)
sudo /usr/local/bin/backup.sh
# Test with cron's environment (minimal PATH)
env -i /usr/local/bin/backup.sh
The env -i version simulates cron’s minimal environment more closely. If it fails there but works normally, you have an environment issue.
For scheduled testing, add a temporary job that runs in a few minutes:
# In crontab: run once, 3 minutes from now
$(date -d "+3 minutes" +%M\ %H)\ * * * /usr/local/bin/backup.sh >> /tmp/test.log 2>&1
Check the log after it runs, then remove the test entry.
anacron for Servers That Aren’t Always On
Cron misses jobs when the system is off at the scheduled time. anacron runs jobs based on how long since they last ran, not at specific times. It’s designed for systems that aren’t always on.
cat /etc/anacrontab
# period delay job-identifier command
1 5 cron.daily run-parts --report /etc/cron.daily
7 10 cron.weekly run-parts --report /etc/cron.weekly
@monthly 15 cron.monthly run-parts --report /etc/cron.monthly
period is days between runs. delay is minutes to wait after boot before running. On a desktop or laptop, anacron ensures daily tasks eventually run even if the machine was off at the usual time.
For servers, systemd timers with Persistent=true are a better alternative to anacron — they’re integrated with the systemd ecosystem and have better logging. The systemd timers guide covers this in detail.
Checking if Cron Is Running
systemctl status cron # Ubuntu/Debian
systemctl status crond # CentOS/RHEL
Check recent cron activity:
grep CRON /var/log/syslog | tail -20 # Debian/Ubuntu
grep CRON /var/log/cron | tail -20 # CentOS/RHEL
Each cron execution shows in the log. If your expected jobs aren’t appearing, that’s a sign something’s wrong with the crontab format or the cron daemon itself.
Useful crontab Shortcuts
Some implementations support shortcuts:
@reboot /usr/local/bin/startup.sh # Run once at boot
@hourly /usr/local/bin/hourly.sh # Run once per hour
@daily /usr/local/bin/daily.sh # Run once per day at midnight
@weekly /usr/local/bin/weekly.sh # Run once per week
@monthly /usr/local/bin/monthly.sh # Run once per month
@reboot is particularly useful for starting custom services, setting up networking, or running initialization scripts on boot.