Cron has been scheduling Unix tasks since 1975. It’s simple, reliable, and will silently fail in ways that waste hours of debugging. Here’s how to use it properly.
Cron Syntax# │ │ │ │ │ └ ─ │ │ │ │ └ ─ ─ ─ │ │ │ └ ─ ─ ─ ─ ─ │ │ └ ─ ─ ─ ─ ─ ─ ─ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ c ─ ─ ─ ─ ─ o m D M D H M m a o a o i a y n y u n n t r u d o h o t f f ( e ( 0 w 1 m - ( e - o 2 0 e 1 n 3 - k 2 t ) 5 ) h 9 ( ) 0 ( - 1 7 - , 3 1 0 ) a n d 7 a r e S u n d a y )
Common Schedules# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Every minute
* * * * * /script.sh
# Every 5 minutes
*/5 * * * * /script.sh
# Every hour at minute 0
0 * * * * /script.sh
# Daily at 3 AM
0 3 * * * /script.sh
# Weekly on Sunday at midnight
0 0 * * 0 /script.sh
# Monthly on the 1st at 6 AM
0 6 1 * * /script.sh
# Every weekday at 9 AM
0 9 * * 1-5 /script.sh
# Every 15 minutes during business hours
*/15 9-17 * * 1-5 /script.sh
The Silent Failure Problem# Cron’s default behavior: run command, discard output, send errors to email (which you probably haven’t configured).
Always redirect output:
1
2
3
4
5
6
7
8
# Log everything
0 3 * * * /backup.sh >> /var/log/backup.log 2>& 1
# Log with timestamp
0 3 * * * /backup.sh 2>& 1 | ts '[%Y-%m-%d %H:%M:%S]' >> /var/log/backup.log
# Discard output only if you really don't care
0 * * * * /cleanup.sh > /dev/null 2>& 1
Environment Gotcha# Cron runs with a minimal environment. Your script works in terminal but fails in cron because:
PATH is minimal (/usr/bin:/bin)No user profile loaded No environment variables from .bashrc Fix 1: Use full paths
1
2
3
4
5
# Bad
0 * * * * python /scripts/task.py
# Good
0 * * * * /usr/bin/python3 /scripts/task.py
Fix 2: Set PATH in crontab
1
2
3
4
PATH = /usr/local/bin:/usr/bin:/bin
SHELL = /bin/bash
0 * * * * /scripts/task.sh
Fix 3: Source environment in script
1
2
3
#!/bin/bash
source /home/user/.bashrc
# rest of script
Lock Files (Prevent Overlap)# If a job runs longer than its interval, you get overlapping runs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
LOCKFILE = /tmp/myjob.lock
# Exit if already running
if [ -f " $LOCKFILE " ] ; then
echo "Already running"
exit 1
fi
# Create lock
trap "rm -f $LOCKFILE " EXIT
touch " $LOCKFILE "
# Your actual work here
/do/the/thing
Or use flock:
1
0 * * * * flock -n /tmp/myjob.lock /scripts/myjob.sh
The -n flag makes it fail immediately if lock is held.
Timeout Protection# Prevent runaway jobs:
1
0 * * * * timeout 3600 /scripts/long-job.sh
Kills the job after 1 hour.
Error Notification# Get notified when things fail:
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
set -e # Exit on error
cleanup() {
if [ $? -ne 0 ] ; then
echo "Job failed at $( date) " | mail -s "Cron failure: backup" admin@example.com
fi
}
trap cleanup EXIT
# Your job here
/run/backup
Or use a monitoring service:
1
0 3 * * * /backup.sh && curl -s https://hc-ping.com/your-uuid
Logging Best Practices# 1
2
3
4
5
6
7
8
9
10
#!/bin/bash
LOG = /var/log/myjob.log
exec 1>>$LOG 2>& 1 # Redirect all output to log
echo "=== Started at $( date) ==="
# Your commands here
/do/stuff
echo "=== Finished at $( date) ==="
Rotate logs:
1
2
3
4
5
6
7
8
# /etc/logrotate.d/myjob
/var/log/myjob.log {
weekly
rotate 4
compress
missingok
notifempty
}
Testing Cron Jobs# Test the command manually first:
1
2
# Run exactly as cron would
env -i /bin/bash -c '/path/to/script.sh'
Check cron is running:
1
2
3
systemctl status cron
# or
systemctl status crond # RHEL/CentOS
Watch the logs:
1
2
3
tail -f /var/log/syslog | grep CRON
# or
journalctl -u cron -f
List your crontab:
User vs System Crontabs# User crontab (crontab -e):
Stored in /var/spool/cron/crontabs/ Runs as that user No user field in syntax System crontab (/etc/crontab):
Has extra user field Runs as specified user 1
2
3
4
5
# /etc/crontab has user field
0 3 * * * root /scripts/backup.sh
# User crontab doesn't
0 3 * * * /scripts/backup.sh
Drop-in directory (/etc/cron.d/):
Like system crontab (has user field) One file per job Easier to manage with config management Cron Alternatives# Systemd timers (modern Linux):
1
2
3
4
5
6
7
8
9
10
# /etc/systemd/system/backup.timer
[Unit]
Description = Daily backup
[Timer]
OnCalendar = daily
Persistent = true
[Install]
WantedBy = timers.target
1
2
3
4
5
6
7
# /etc/systemd/system/backup.service
[Unit]
Description = Backup job
[Service]
Type = oneshot
ExecStart = /scripts/backup.sh
1
2
systemctl enable --now backup.timer
systemctl list-timers
Benefits: better logging, dependency management, resource controls.
Common Mistakes# Wrong time zone:
1
2
3
# Set in crontab
TZ = America/New_York
0 9 * * * /morning-job.sh
Day-of-month AND day-of-week:
1
2
3
4
# This runs if EITHER matches (not both!)
0 0 15 * 5 # 15th of month OR Fridays
# For "15th if it's a Friday", use a wrapper script
Forgetting the newline:
Crontab needs a trailing newline after the last entry.
Special characters in commands:
1
2
# Percent signs need escaping
0 * * * * echo " $( date +\% Y-\% m-\% d) " >> /log
Production Checklist# Cron is a 50-year-old tool that still runs critical infrastructure. Respect it by setting it up properly.