Cron is the classic Unix job scheduler — a daemon that wakes up every minute, checks its tables, and runs commands whose time has come. It has been part of Unix since the 1970s and remains the backbone of scheduled automation on Linux servers worldwide. Understanding cron thoroughly — syntax, user vs system cron, environment gotchas, and logging — is a core sysadmin skill.
The cron daemon is crond on RHEL-family
systems and cron on Debian/Ubuntu. Both read the same crontab format.
| Operator | Meaning | Example | Result |
|---|---|---|---|
| * | Every value | * in minute | Every minute |
| , | List of values | 1,15,30 in minute | At minutes 1, 15, and 30 |
| - | Range of values | 9-17 in hour | Hours 9 through 17 |
| / | Step/interval | */15 in minute | Every 15 minutes |
| */2 | Every Nth | */2 in hour | Every 2 hours |
| 1-5/2 | Range with step | 1-5/2 in hour | Hours 1, 3, 5 |
| String | Equivalent | Meaning |
|---|---|---|
| @reboot | — | Run once at system startup |
| @yearly | 0 0 1 1 * | Once a year, Jan 1 midnight |
| @annually | 0 0 1 1 * | Same as @yearly |
| @monthly | 0 0 1 * * | First day of each month, midnight |
| @weekly | 0 0 * * 0 | Every Sunday at midnight |
| @daily | 0 0 * * * | Every day at midnight |
| @midnight | 0 0 * * * | Same as @daily |
| @hourly | 0 * * * * | Every hour on the hour |
# Edit YOUR crontab (opens in $EDITOR) crontab -e # List your current crontab crontab -l # Remove your crontab entirely crontab -r # Edit another user's crontab (as root) sudo crontab -e -u alice sudo crontab -l -u alice sudo crontab -r -u alice # Install a crontab from a file crontab mycrontab.txt # Backup your crontab crontab -l > ~/mycrontab.bak # Check cron daemon status sudo systemctl status crond # RHEL/Rocky sudo systemctl status cron # Debian/Ubuntu
crontab -r does not ask "are you sure?"
— it silently deletes your entire crontab immediately. Always backup
first with crontab -l > backup.txt. An easy typo is
-r when you meant -e.
# Run every minute (useful for testing — remove after!) * * * * * /usr/local/bin/myscript.sh # Every 5 minutes */5 * * * * /usr/local/bin/check-service.sh # Every day at 2:30 AM 30 2 * * * /usr/local/bin/nightly-backup.sh # Every weekday (Mon-Fri) at 8 AM 0 8 * * 1-5 /usr/local/bin/send-report.sh # Every Monday at 6 AM 0 6 * * 1 /usr/local/bin/weekly-cleanup.sh # First day of every month at midnight 0 0 1 * * /usr/local/bin/monthly-report.sh # Twice a day — 6 AM and 6 PM 0 6,18 * * * /usr/local/bin/sync-data.sh # Every 15 minutes during business hours (8 AM - 6 PM) */15 8-18 * * 1-5 /usr/local/bin/poll-queue.sh # Once a year — Jan 1 at midnight @yearly /usr/local/bin/annual-cleanup.sh # At every reboot @reboot /usr/local/bin/startup-tasks.sh
Cron has multiple layers — knowing which one to use matters:
# USER CRONTABS — managed with crontab -e # Stored in /var/spool/cron/crontabs/ (per user) # Format: minute hour dom month dow command # Run as the user who owns the crontab # SYSTEM CRONTAB — /etc/crontab # Has an extra USERNAME field: # minute hour dom month dow USERNAME command cat /etc/crontab/etc/crontab format:
# /etc/cron.d/ — drop-in cron files (same format as /etc/crontab) # Best place for package and system-level scheduled jobs ls /etc/cron.d/ # /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/ # Drop executable scripts here — run by run-parts at the time above ls /etc/cron.daily/ ls /etc/cron.weekly/
The most common reason cron jobs fail when run manually they work fine — cron runs with a minimal environment:
# Cron's environment is minimal — very different from your shell # Check what cron sees by logging it * * * * * env > /tmp/cron-environment.txt # Typical cron environment: # HOME=/root # LOGNAME=root # PATH=/usr/bin:/bin <-- much shorter than your interactive PATH # SHELL=/bin/sh <-- sh, not bash! # SOLUTIONS: # 1. Set PATH at top of crontab PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 30 2 * * * myscript.sh # 2. Use full paths in cron commands (most reliable) 30 2 * * * /usr/local/bin/myscript.sh # 3. Set SHELL explicitly SHELL=/bin/bash 30 2 * * * myscript.sh # 4. Source your profile inside the script # #!/bin/bash # source /etc/profile # source ~/.bash_profile
/usr/local/bin in PATH — cron may not.
Commands like python3, node, or custom
scripts in /usr/local/bin will fail with "command not found"
in cron unless you use the full path or set PATH explicitly.
# By default cron emails output to the user running the job # Set MAILTO to redirect or suppress # Send output to a specific address MAILTO=sysadmin@example.com 30 2 * * * /usr/local/bin/backup.sh # Suppress all email (discard stdout and stderr) MAILTO="" 30 2 * * * /usr/local/bin/backup.sh # Redirect output to a log file instead 30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 # Log with timestamp 30 2 * * * echo "$(date): Starting backup" >> /var/log/backup.log; \ /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 # Send errors to log but discard normal output 30 2 * * * /usr/local/bin/backup.sh > /dev/null 2>> /var/log/backup-errors.log
#!/bin/bash # nightly-backup.sh — example production cron script # Use full paths — cron has a minimal PATH RSYNC=/usr/bin/rsync LOGGER=/usr/bin/logger DATE=$(date '+%Y-%m-%d %H:%M:%S') # Log function — writes to both syslog and log file log() { echo "[$DATE] $*" | tee -a /var/log/nightly-backup.log $LOGGER -t nightly-backup "$*" } # Lock file — prevent overlapping runs LOCKFILE=/var/run/nightly-backup.lock if [ -f "$LOCKFILE" ]; then log "ERROR: Already running (lockfile exists). Exiting." exit 1 fi touch "$LOCKFILE" trap 'rm -f "$LOCKFILE"' EXIT log "Starting nightly backup" # Do the work $RSYNC -av --delete /data/ /backup/nightly/ >> /var/log/nightly-backup.log 2>&1 EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then log "Backup completed successfully" else log "ERROR: Backup failed with exit code $EXIT_CODE" exit $EXIT_CODE fi
# /etc/cron.d/ files use the same format as /etc/crontab # (includes the username field) # Create a system cron job for log rotation check sudo tee /etc/cron.d/logcheck << 'EOF' SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin MAILTO=sysadmin@example.com # minute hour dom month dow user command 0 6 * * * root /usr/sbin/logcheck EOF # Correct permissions — cron.d files must not be world-writable sudo chmod 644 /etc/cron.d/logcheck sudo chown root:root /etc/cron.d/logcheck # Verify cron picks it up (no restart needed) sudo systemctl status crond sudo ls -la /etc/cron.d/
/etc/cron.d/ that are world-writable
or not owned by root. Always set chmod 644 and
chown root:root on files you drop there.
# Control which users can use crontab # /etc/cron.allow — whitelist (if exists, ONLY these users can use cron) sudo tee /etc/cron.allow << 'EOF' root alice bob EOF # /etc/cron.deny — blacklist (these users cannot use cron) sudo tee /etc/cron.deny << 'EOF' tempuser contractor1 EOF # Logic: # cron.allow exists → only listed users can use cron # cron.allow absent, cron.deny exists → everyone except listed users # Neither file exists → only root can use cron (RHEL default) # Both empty → no one can use cron # List all user crontabs on the system sudo ls -la /var/spool/cron/ sudo ls -la /var/spool/cron/crontabs/ # Debian/Ubuntu path
| Command | What it does |
|---|---|
| crontab -e | Edit your crontab |
| crontab -l | List your crontab |
| crontab -r | Remove your crontab (no confirmation!) |
| crontab -e -u user | Edit another user's crontab (root) |
| crontab -l > backup.txt | Backup crontab to file |
| systemctl status crond | Check cron daemon (RHEL) |
| systemctl status cron | Check cron daemon (Debian) |
| ls /etc/cron.d/ | List system drop-in cron jobs |
| ls /etc/cron.daily/ | List daily run-parts scripts |
| run-parts /etc/cron.daily | Manually run daily cron scripts |
| journalctl -u crond | View cron daemon logs (RHEL) |
| grep CRON /var/log/syslog | View cron logs (Debian) |
| Location | Purpose | Format |
|---|---|---|
| /var/spool/cron/ | User crontabs | 5 fields + command |
| /etc/crontab | System crontab | 5 fields + user + command |
| /etc/cron.d/ | System drop-in jobs | 5 fields + user + command |
| /etc/cron.hourly/ | Scripts run every hour | Executable script |
| /etc/cron.daily/ | Scripts run daily | Executable script |
| /etc/cron.weekly/ | Scripts run weekly | Executable script |
| /etc/cron.monthly/ | Scripts run monthly | Executable script |