The Problem
You need a database backup every night at 2 AM, log cleanup every Sunday, SSL certificate renewal checks twice daily, and a health check endpoint pinged every 5 minutes. Cron is the standard Linux scheduler, but its syntax is cryptic, its error handling is non-existent by default, and debugging silent failures wastes hours.
Crontab Syntax
┌───────────── 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
| Character | Meaning | Example |
|---|---|---|
| <code class="inline-code"><em></code> | Every value | <code class="inline-code"></em> <em> </em> <em> </em></code> = every minute |
| <code class="inline-code">,</code> | List | <code class="inline-code">1,15,30</code> = at minutes 1, 15, and 30 |
| <code class="inline-code">-</code> | Range | <code class="inline-code">9-17</code> = hours 9 through 17 |
| <code class="inline-code">/</code> | Step | <code class="inline-code">*/5</code> = every 5 units |
Managing crontab
crontab -e # Edit your crontab
crontab -l # List your cron jobs
sudo crontab -e -u deploy # Edit for specific user
crontab -r # Remove all cron jobs
crontab /path/to/crontab-file # Load from file
Common Schedule Patterns
* /opt/scripts/health-check.sh # Every minute
/5 * /opt/scripts/check-queue.sh # Every 5 minutes
0 /opt/scripts/sync-data.sh # Every hour
30 2 * /opt/scripts/backup.sh # Daily at 2:30 AM
0 9 1 /opt/scripts/weekly-report.sh # Monday 9 AM
0 18 1-5 /opt/scripts/daily-summary.sh # Weekdays 6 PM
0 0 1 /opt/scripts/monthly-cleanup.sh # First of month
/15 9-17 * 1-5 /opt/scripts/monitor.sh # Every 15 min, business hours
0 6,18 * /opt/scripts/cert-check.sh # Twice daily
0 3 0 /opt/scripts/weekly-maintenance.sh # Sunday 3 AM
0 0 1 1,4,7,10 * /opt/scripts/quarterly-audit.sh # Quarterly
Special strings
@reboot /opt/scripts/startup.sh # Run once at startup
@hourly /opt/scripts/hourly.sh # 0
@daily /opt/scripts/daily.sh # 0 0 *
@weekly /opt/scripts/weekly.sh # 0 0 0
@monthly /opt/scripts/monthly.sh # 0 0 1
Production Cron Job Examples
Database backup with retention
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/backups/postgres"
RETENTION_DAYS=14
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="production_app"
pg_dump -Fc "$DB_NAME" > "${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump"
if [ ! -s "${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump" ]; then
echo "ERROR: Backup file is empty" >&2
exit 1
fi
find "$BACKUP_DIR" -name "*.dump" -mtime +${RETENTION_DAYS} -delete
echo "Backup completed: ${DB_NAME}_${TIMESTAMP}.dump"
30 2 * /opt/scripts/backup-postgres.sh >> /var/log/cron/backup.log 2>&1
Log rotation and cleanup
0 3 find /var/log/myapp -name ".log" -mtime +7 -delete
0 2 find /var/log/myapp -name ".log" -mtime +1 ! -name "*.gz" -exec gzip {} \;
SSL certificate expiry check
#!/bin/bash
set -euo pipefail
DOMAINS=("app.example.com" "api.example.com" "admin.example.com")
ALERT_DAYS=14
for domain in "${DOMAINS[@]}"; do
expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$expiry" ]; then
echo "WARNING: Could not check $domain"
continue
fi
expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$ALERT_DAYS" ]; then
echo "ALERT: $domain SSL expires in $days_left days ($expiry)"
fi
done
0 6,18 * /opt/scripts/check-ssl.sh | mail -s "SSL Check" ops@company.com
Docker cleanup
0 4 * docker system prune -f >> /var/log/cron/docker-cleanup.log 2>&1
0 4 0 docker system prune -af --volumes >> /var/log/cron/docker-cleanup.log 2>&1
Disk space monitoring
/10 * df -h | awk '$5+0 > 85 {print "DISK ALERT: "$6" at "$5}' | mail -s "Disk Space Alert $(hostname)" ops@company.com
Error Handling and Logging
Redirect output
# Both stdout and stderr to log
30 2 * /opt/scripts/backup.sh >> /var/log/cron/backup.log 2>&1
# With timestamp
30 2 * /opt/scripts/backup.sh 2>&1 | ts '[%Y-%m-%d %H:%M:%S]' >> /var/log/cron/backup.log
Prevent overlapping runs with flock
/5 * /usr/bin/flock -n /tmp/queue-worker.lock /opt/scripts/process-queue.sh
/5 * /usr/bin/flock -w 300 /tmp/sync.lock /opt/scripts/sync-data.sh
Set environment variables
# Set at top of crontab
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=ops@company.com
HOME=/home/deploy
# Or source profile in command
30 2 * source /home/deploy/.profile && /opt/scripts/backup.sh
Error notification pattern
#!/bin/bash
set -euo pipefail
LOG_FILE="/var/log/cron/backup-$(date +%Y%m%d).log"
run_backup() {
echo "[$(date)] Starting backup..."
pg_dump -Fc production > /backups/production_$(date +%Y%m%d).dump
echo "[$(date)] Backup completed successfully"
}
if ! run_backup >> "$LOG_FILE" 2>&1; then
echo "Backup FAILED at $(date). Check $LOG_FILE" | \
mail -s "CRITICAL: Backup Failed on $(hostname)" ops@company.com
exit 1
fi
Debugging Cron Failures
# Check if cron is running
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/CentOS
# Check cron logs
grep CRON /var/log/syslog
grep CRON /var/log/cron
journalctl -u cron --since today
# Test with cron's environment
sudo -u deploy env -i /bin/bash -c '/opt/scripts/backup.sh'
env -i HOME=/home/deploy SHELL=/bin/bash PATH=/usr/bin:/bin /opt/scripts/backup.sh
# Common fixes
chmod +x /opt/scripts/backup.sh # Script not executable
# Use full paths: /usr/bin/pg_dump not pg_dump
# Escape percent: date +\%Y\%m\%d not date +%Y%m%d
Common Mistakes
/usr/bin/python3 not python3.% in crontab — The % character is interpreted as a newline. Escape with \% or put the command in a script..bashrc or .profile. Set variables explicitly.flock.Quick Reference
| Schedule | Crontab Expression |
|---|---|
| Every minute | <code class="inline-code"><em> </em> <em> </em> *</code> |
| Every 5 minutes | <code class="inline-code"><em>/5 </em> <em> </em> *</code> |
| Every hour | <code class="inline-code">0 <em> </em> <em> </em></code> |
| Daily at 2 AM | <code class="inline-code">0 2 <em> </em> *</code> |
| Weekdays at 9 AM | <code class="inline-code">0 9 <em> </em> 1-5</code> |
| Every Sunday 3 AM | <code class="inline-code">0 3 <em> </em> 0</code> |
| First of month | <code class="inline-code">0 0 1 <em> </em></code> |
| Every 15 min, business hours | <code class="inline-code"><em>/15 9-17 </em> * 1-5</code> |
| Twice daily | <code class="inline-code">0 6,18 <em> </em> *</code> |
| On reboot | <code class="inline-code">@reboot</code> |
Summary
Cron is reliable but unforgiving. Always use full paths, redirect output to log files, prevent overlapping runs with flock, and test commands manually with cron's minimal environment before adding them to crontab. For critical jobs, add alerting so you know immediately when something fails.
---
Frequently Asked Questions
How do I edit the crontab in Linux?
Run crontab -e to open your user's crontab file in the default editor. Each line follows the format minute hour day month weekday command. Use crontab -l to list existing cron jobs and crontab -r to remove all entries. For system-wide jobs, edit files in /etc/cron.d/ instead.
What does the cron expression "0 /2 " mean?
This runs the command at minute 0 of every 2nd hour (00:00, 02:00, 04:00, etc.). The /2 means "every 2 units" in the hour field. Common patterns: /5 (every 5 minutes), 0 0 (daily at midnight), 0 0 * 0 (weekly on Sunday at midnight).
Why is my cron job not running?
Common causes include: the script lacks a full path to commands (cron has a minimal PATH), file permissions aren't set to executable, environment variables aren't loaded (cron doesn't source .bashrc), or the cron service isn't running. Check /var/log/syslog or /var/log/cron for execution logs and errors.
How do I redirect cron job output to a log file?
Append >> /var/log/myjob.log 2>&1 to your crontab entry to capture both stdout and stderr. Use > instead of >> to overwrite rather than append. To suppress all output, redirect to /dev/null: * /path/script.sh > /dev/null 2>&1. For emailed output, ensure a local MTA is configured.
What is the difference between crontab and systemd timers?
Crontab is simpler with a one-line-per-job format, while systemd timers offer more features like random delays, dependency management, and resource controls through service units. Systemd timers integrate with journald for logging and can trigger on events beyond time (boot, socket activation). Use crontab for simple scheduled tasks and systemd timers for complex service management.
---
Related Resources
- Linux Commands Reference — 50+ Linux commands with production examples
- Cron Expression Builder — Visual cron expression builder and validator
- DevOps Interview Academy — Linux interview questions
- systemctl Service Management — Combining cron with systemd services
- Linux Process Management Guide — Managing processes spawned by cron