Linux Cron Jobs: Scheduling Tasks
Cron is Unix's time-based job scheduler, running continuously in the background as a daemon. It's the workhorse of system automation, handling everything from nightly database backups to log rotation...
Key Insights
- Cron uses a five-field syntax (minute, hour, day, month, weekday) that’s cryptic at first but becomes second nature—learn the special characters (*, -, /, ,) and you can schedule anything from database backups to system health checks
- The most common cron failures stem from environment differences: cron jobs run with minimal PATH and environment variables, so always use absolute paths and set variables explicitly in your crontab
- For production systems, implement proper logging, use flock to prevent job overlaps, and consider systemd timers for complex scheduling needs—don’t just set it and forget it
Understanding Cron Fundamentals
Cron is Unix’s time-based job scheduler, running continuously in the background as a daemon. It’s the workhorse of system automation, handling everything from nightly database backups to log rotation and system maintenance tasks. If you need something to happen automatically at specific intervals, cron is your tool.
The beauty of cron lies in its simplicity. You define when a command should run using a straightforward syntax, and cron handles the rest. No complex frameworks, no dependencies—just reliable, predictable task execution that’s been battle-tested for decades.
Mastering Cron Syntax
Cron’s scheduling syntax uses five fields representing time units:
* * * * * command-to-execute
│ │ │ │ │
│ │ │ │ └─── Day of week (0-7, where 0 and 7 are Sunday)
│ │ │ └───── Month (1-12)
│ │ └─────── Day of month (1-31)
│ └───────── Hour (0-23)
└─────────── Minute (0-59)
Each field accepts specific values, ranges, lists, and special characters:
- Asterisk (*): Matches any value (every minute, every hour, etc.)
- Comma (,): Specifies multiple values (1,15,30)
- Hyphen (-): Defines ranges (1-5 for Monday through Friday)
- Slash (/): Sets step values (*/15 means every 15 units)
Here are common scheduling patterns:
# Every day at midnight
0 0 * * * /path/to/script.sh
# Every 15 minutes
*/15 * * * * /usr/bin/check-status.sh
# Every weekday at 9 AM
0 9 * * 1-5 /home/user/morning-report.sh
# First day of every month at 2:30 AM
30 2 1 * * /opt/scripts/monthly-cleanup.sh
# Every Sunday at 3 AM
0 3 * * 0 /usr/local/bin/weekly-backup.sh
Cron also supports special strings that improve readability:
@reboot # Run once at startup
@yearly # Run once a year (0 0 1 1 *)
@annually # Same as @yearly
@monthly # Run once a month (0 0 1 * *)
@weekly # Run once a week (0 0 * * 0)
@daily # Run once a day (0 0 * * *)
@midnight # Same as @daily
@hourly # Run once an hour (0 * * * *)
Example using special strings:
@daily /usr/local/bin/backup.sh
@reboot /home/user/startup-script.sh
@hourly /opt/monitor/check-services.sh
Managing Crontab
Each user has their own crontab file, and the system has additional crontabs for system-level tasks. You interact with your personal crontab using the crontab command:
# Edit your crontab (opens in default editor)
crontab -e
# List current cron jobs
crontab -l
# Remove all your cron jobs (use with caution!)
crontab -r
# Edit another user's crontab (requires root)
sudo crontab -u username -e
A typical crontab file looks like this:
# Environment variables
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=admin@example.com
# Database backup every night at 2 AM
0 2 * * * /opt/scripts/db-backup.sh
# Clean temporary files every Sunday at 4 AM
0 4 * * 0 /usr/local/bin/cleanup-temp.sh
# Check disk space every hour
0 * * * * /usr/local/bin/disk-check.sh
Setting environment variables at the top of your crontab is crucial. Cron runs with a minimal environment, so explicitly defining PATH, SHELL, and other variables prevents mysterious failures.
Real-World Cron Job Examples
Let’s build practical examples that you can adapt for your systems.
Database Backup Script (Nightly at 2 AM)
# Crontab entry
0 2 * * * /opt/scripts/backup-database.sh >> /var/log/db-backup.log 2>&1
# /opt/scripts/backup-database.sh
#!/bin/bash
BACKUP_DIR="/backups/mysql"
DATE=$(date +\%Y\%m\%d_\%H\%M\%S)
MYSQL_USER="backup_user"
MYSQL_PASS="secure_password"
mkdir -p "$BACKUP_DIR"
mysqldump -u "$MYSQL_USER" -p"$MYSQL_PASS" --all-databases | \
gzip > "$BACKUP_DIR/all-databases-$DATE.sql.gz"
# Keep only last 7 days of backups
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 -delete
echo "$(date): Backup completed successfully"
Log Cleanup (Weekly on Sunday)
# Crontab entry
0 3 * * 0 /usr/local/bin/cleanup-logs.sh
# /usr/local/bin/cleanup-logs.sh
#!/bin/bash
LOG_DIRS=("/var/log/app" "/var/log/nginx" "/var/log/custom")
for dir in "${LOG_DIRS[@]}"; do
find "$dir" -name "*.log" -mtime +30 -delete
find "$dir" -name "*.gz" -mtime +90 -delete
done
# Rotate and compress current logs
for logfile in /var/log/app/*.log; do
[ -f "$logfile" ] || continue
gzip -c "$logfile" > "$logfile.$(date +\%Y\%m\%d).gz"
> "$logfile" # Truncate original
done
System Health Check (Every 5 Minutes)
# Crontab entry
*/5 * * * * /usr/local/bin/health-check.sh
# /usr/local/bin/health-check.sh
#!/bin/bash
ALERT_EMAIL="ops@example.com"
DISK_THRESHOLD=90
CPU_THRESHOLD=80
# Check disk usage
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt "$DISK_THRESHOLD" ]; then
echo "Disk usage at ${DISK_USAGE}% on $(hostname)" | \
mail -s "ALERT: High Disk Usage" "$ALERT_EMAIL"
fi
# Check CPU load
CPU_LOAD=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | sed 's/,//')
# Additional checks as needed
Handling Output and Logging
By default, cron emails command output to the user. For production systems, redirect output to log files:
# Redirect stdout and stderr to log file
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Separate stdout and stderr
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>> /var/log/backup-error.log
# Discard all output (use sparingly)
0 2 * * * /opt/scripts/backup.sh > /dev/null 2>&1
# Suppress email but keep stderr
0 2 * * * /opt/scripts/backup.sh > /dev/null
For better logging, add timestamps to your scripts:
#!/bin/bash
log() {
echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] $*"
}
log "Starting backup process"
# Your backup logic here
log "Backup completed"
Disable email notifications by setting MAILTO to empty:
MAILTO=""
0 2 * * * /opt/scripts/silent-job.sh
Troubleshooting Cron Jobs
The most common issue is environment differences. Cron jobs run with minimal PATH and environment variables:
# Always set PATH explicitly
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Or use absolute paths in commands
0 2 * * * /usr/bin/python3 /opt/scripts/backup.py
Check cron execution logs:
# On Debian/Ubuntu
sudo tail -f /var/log/syslog | grep CRON
# On RHEL/CentOS
sudo tail -f /var/log/cron
# Check for specific user
sudo grep CRON /var/log/syslog | grep username
Create a debugging wrapper to capture environment:
# /usr/local/bin/cron-debug-wrapper.sh
#!/bin/bash
echo "=== Cron Debug $(date) ===" >> /tmp/cron-debug.log
echo "PATH: $PATH" >> /tmp/cron-debug.log
echo "USER: $USER" >> /tmp/cron-debug.log
echo "HOME: $HOME" >> /tmp/cron-debug.log
env >> /tmp/cron-debug.log
echo "=== End Debug ===" >> /tmp/cron-debug.log
# Then in crontab:
* * * * * /usr/local/bin/cron-debug-wrapper.sh
Always test scripts manually before scheduling:
# Run as the same user that cron will use
sudo -u www-data /opt/scripts/backup.sh
# Test with minimal environment
env -i /bin/bash --noprofile --norc /opt/scripts/backup.sh
Best Practices and Modern Alternatives
Prevent concurrent job executions using flock:
# Crontab entry
*/5 * * * * /usr/bin/flock -n /tmp/myjob.lock /opt/scripts/myjob.sh
# Or within the script
#!/bin/bash
LOCKFILE=/tmp/myjob.lock
exec 200>"$LOCKFILE"
flock -n 200 || exit 1
# Your job logic here
For complex scheduling, consider systemd timers as a modern alternative:
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup timer
[Timer]
OnCalendar=daily
OnCalendar=02:00
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Database backup service
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup
Enable the timer:
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
sudo systemctl list-timers
Here’s a production-ready cron job template:
#!/bin/bash
set -euo pipefail
LOCKFILE="/tmp/$(basename "$0").lock"
LOGFILE="/var/log/$(basename "$0" .sh).log"
# Logging function
log() {
echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] $*" | tee -a "$LOGFILE"
}
# Acquire lock
exec 200>"$LOCKFILE"
if ! flock -n 200; then
log "ERROR: Another instance is running"
exit 1
fi
# Cleanup on exit
trap 'rm -f "$LOCKFILE"' EXIT
log "Starting job"
# Your job logic here
log "Job completed successfully"
Cron remains the standard for task scheduling on Linux systems. Master the syntax, handle environment variables properly, implement robust logging, and you’ll have reliable automation that runs for years without intervention.