Time-based job scheduler in Unix-like operating systems for automating recurring tasks.
Table of Contents#
- Overview
- Cron Expression Syntax
- 2.1 Field Values
- 2.2 Special Characters
- 2.3 Examples
- Managing Crontabs
- 3.1 User Crontabs
- 3.2 System Crontabs
- 3.3 Cron Directories
- Environment Variables
- Output Redirection and Logging
- Debugging Failed Jobs
- Systemd Timers as Alternative
- Edge Cases and Pitfalls
- Troubleshooting
- See Also
- Sources
1. Overview#
Cron is a daemon (crond or cron) that runs in the background on Unix-like systems and executes scheduled commands at specified times. It reads configuration files called "crontabs" (cron tables) that define when and what to run.
Cron is used for:
- Automated backups
- Log rotation and cleanup
- System maintenance tasks
- Periodic data processing
- Monitoring and health checks
The cron daemon checks for scheduled jobs every minute and executes any that match the current time.
2. Cron Expression Syntax#
A cron expression is a string consisting of five fields that each define a specific unit of time:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12 or JAN-DEC)
│ │ │ │ ┌───────────── day of week (0-6, 0=Sunday, or SUN-SAT)
│ │ │ │ │
* * * * * command to execute2.1 Field Values#
| Field | Allowed Values | Description |
|---|---|---|
| Minute | 0-59 | Minute within the hour |
| Hour | 0-23 | Hour of the day (24-hour format) |
| Day of month | 1-31 | Day within the month |
| Month | 1-12 or JAN-DEC | Month of the year |
| Day of week | 0-6 or SUN-SAT | Day of the week (0 and 7 both mean Sunday) |
Note: Day of week values vary across implementations. In standard cron, 0 = Sunday, 1 = Monday, through 6 = Saturday. The value 7 is also accepted as Sunday on most systems.
2.2 Special Characters#
| Character | Description | Example |
|---|---|---|
* | Matches every value in the field | * * * * * (every minute) |
, | List separator for multiple values | 1,15 * * * * (minute 1 and 15) |
- | Specifies a range of values | 0 8-17 * * * (hours 8 through 17) |
/ | Step value (increment) | */5 * * * * (every 5 minutes) |
Step values explained: The / character defines increments. */5 in the minute field means "every 5 minutes" (0, 5, 10, 15, ...). You can combine ranges with steps: 0-30/10 means "every 10 minutes during the first half hour" (0, 10, 20, 30).
2.3 Examples#
* * * * * Every minute
0 * * * * Every hour (at minute 0)
0 0 * * * Every day at midnight
0 0 1 * * First day of every month at midnight
0 0 1 1 * January 1st at midnight
30 20 * * SAT Every Saturday at 20:30
30 20 * * 6 Every Saturday at 20:30 (numeric)
*/5 * * * * Every 5 minutes
*/15 * * * * Every 15 minutes
0 8-17/1 * * * Every hour from 08:00 to 17:00
0 9,18 * * 1-5 At 09:00 and 18:00, Monday through Friday
0 0 */2 * * Every 2 days at midnight
30 4 1,15 * * At 04:30 on the 1st and 15th of every monthPredefined Schedules#
Some cron implementations support shorthand strings:
| String | Equivalent | Description |
|---|---|---|
@reboot | N/A | Run once at startup |
@yearly / @annually | 0 0 1 1 * | Once a year (Jan 1, midnight) |
@monthly | 0 0 1 * * | Once a month (1st, midnight) |
@weekly | 0 0 * * 0 | Once a week (Sunday, midnight) |
@daily / @midnight | 0 0 * * * | Once a day (midnight) |
@hourly | 0 * * * * | Once an hour (minute 0) |
3. Managing Crontabs#
3.1 User Crontabs#
Each user can have their own crontab, managed with the crontab command:
# Edit the current user's crontab (opens in $EDITOR)
crontab -e
# List the current user's crontab
crontab -l
# Remove the current user's crontab (all entries deleted)
crontab -r
# Edit another user's crontab (requires root)
sudo crontab -u username -e
# List another user's crontab
sudo crontab -u username -l
# Load a crontab from a file
crontab mycrontab.txtUser crontab files are stored in /var/spool/cron/ (RHEL/Arch) or /var/spool/cron/crontabs/ (Debian/Ubuntu). Do not edit these files directly; use crontab -e.
User crontab format (no user field):
# m h dom mon dow command
*/5 * * * * /home/user/scripts/check_disk.sh
0 3 * * * /home/user/scripts/backup.sh3.2 System Crontabs#
The system crontab /etc/crontab has an additional user field:
# m h dom mon dow user command
*/5 * * * * root /usr/local/bin/check_disk.sh
0 3 * * * backup /opt/scripts/backup.sh
17 * * * * root cd / && run-parts --report /etc/cron.hourly3.3 Cron Directories#
Most distributions provide drop-in directories for scheduled tasks:
| Directory | Schedule | Format |
|---|---|---|
/etc/cron.d/ | Custom (crontab format with user field) | Crontab entries |
/etc/cron.hourly/ | Every hour | Executable scripts |
/etc/cron.daily/ | Once per day | Executable scripts |
/etc/cron.weekly/ | Once per week | Executable scripts |
/etc/cron.monthly/ | Once per month | Executable scripts |
Files in /etc/cron.d/ use the same format as /etc/crontab (with the user field). Files in the other directories are plain executable scripts run by run-parts.
To add a job via /etc/cron.d/:
# /etc/cron.d/my-backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 2 * * * root /opt/scripts/backup.sh >> /var/log/backup.log 2>&1Scripts in /etc/cron.daily/ and similar directories must:
- Be executable (
chmod +x) - Not have a file extension (
.shbreaksrun-partson some distributions) - Have no dots in the filename on Debian-based systems
4. Environment Variables#
Cron jobs run with a minimal environment, not the user's full login shell environment. This is the most common source of "works on the command line but not in cron" issues.
Default cron environment:
SHELL=/bin/sh
PATH=/usr/bin:/bin
HOME=/home/<user>
LOGNAME=<user>Set environment variables at the top of the crontab:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
HOME=/home/myuser
# Jobs below use the above environment
0 3 * * * /opt/scripts/backup.shKey variables:
| Variable | Purpose |
|---|---|
SHELL | Shell used to execute commands (default: /bin/sh) |
PATH | Search path for commands (default is very limited) |
MAILTO | Email address for job output; empty string disables email |
HOME | Working directory for jobs |
CRON_TZ | Timezone for schedule evaluation (some implementations) |
Best practice: always use absolute paths in cron jobs, or set PATH explicitly in the crontab.
5. Output Redirection and Logging#
By default, cron emails any output (stdout and stderr) to the crontab owner. If no MTA is configured, this output is silently lost.
# Redirect stdout and stderr to a log file
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Redirect only stderr to a log file, discard stdout
0 3 * * * /opt/scripts/backup.sh > /dev/null 2>> /var/log/backup_errors.log
# Discard all output
0 3 * * * /opt/scripts/backup.sh > /dev/null 2>&1
# Send output via email (default behavior, requires MTA)
MAILTO=admin@example.com
0 3 * * * /opt/scripts/backup.sh
# Disable email for all jobs
MAILTO=""Using logger to send output to syslog:
0 3 * * * /opt/scripts/backup.sh 2>&1 | logger -t backup-cron6. Debugging Failed Jobs#
When a cron job does not run or produces unexpected results:
Check the cron log:
# systemd journal journalctl -u cron journalctl -u cronie # Traditional syslog grep CRON /var/log/syslog grep CRON /var/log/cronVerify the cron daemon is running:
systemctl status cron systemctl status cronieTest the command manually as the cron user:
# Simulate cron's minimal environment env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME=/home/user /opt/scripts/backup.shCheck file permissions:
# Script must be executable chmod +x /opt/scripts/backup.sh # Crontab file must not be world-writable ls -la /var/spool/cron/Check for syntax errors in the crontab:
# Validate by listing (syntax errors shown on load) crontab -lCheck
/etc/cron.allowand/etc/cron.deny: If/etc/cron.allowexists, only listed users can use cron. If it does not exist,/etc/cron.denyblocks listed users.
7. Systemd Timers as Alternative#
Systemd timers are a modern alternative to cron with better logging, dependency management, and resource control.
Create a service unit (/etc/systemd/system/backup.service):
[Unit]
Description=Daily backup job
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backupCreate a timer unit (/etc/systemd/system/backup.timer):
[Unit]
Description=Run backup daily at 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.targetEnable and start the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
# Check timer status
systemctl list-timers
systemctl status backup.timer
# View logs
journalctl -u backup.serviceComparison:
| Feature | Cron | Systemd Timer |
|---|---|---|
| Logging | Email or manual redirect | journalctl (automatic) |
| Dependencies | None | Full systemd dependency support |
| Missed runs | Lost | Persistent=true catches up |
| Resource limits | None | CPUQuota, MemoryMax, etc. |
| Randomized delay | None | RandomizedDelaySec |
| Monitoring | Manual | systemctl list-timers |
8. Edge Cases and Pitfalls#
Daylight Saving Time#
- Jobs scheduled during the "spring forward" gap (e.g., 02:30 in a timezone that skips from 02:00 to 03:00) may not run at all
- Jobs during the "fall back" overlap may run twice
- Use UTC for critical jobs: set
CRON_TZ=UTCor useTZ=UTCin the command
Day of Month and Day of Week#
When both day of month and day of week are specified (not *), cron runs the job when either condition is true (OR logic), not when both are true (AND logic):
# Runs on the 1st AND on every Monday (not just Mondays that are the 1st)
0 0 1 * 1 /opt/scripts/job.shPercent Signs#
The % character has special meaning in crontabs: it is converted to a newline, and everything after the first % is sent as stdin. Escape with \%:
# WRONG - % breaks the command
0 0 * * * echo "Today is $(date +%F)"
# CORRECT - escape the percent signs
0 0 * * * echo "Today is $(date +\%F)"Running on Reboot#
@reboot /opt/scripts/startup.sh >> /var/log/startup.log 2>&1Troubleshooting#
| Issue | Cause | Solution |
|---|---|---|
| Job does not run | Cron daemon not running | systemctl start cron or systemctl start cronie |
| Job does not run | Command not found | Use absolute paths; set PATH in crontab |
| Job does not run | Permission denied | chmod +x the script; check /etc/cron.allow |
| Job produces no output | Output not redirected | Add >> /var/log/job.log 2>&1 to the command |
| Job runs at wrong time | Timezone mismatch | Set CRON_TZ or use UTC |
| Job does not run during DST | Spring forward gap | Schedule at a time outside the DST transition window |
% in command breaks it | Unescaped percent sign | Use \% instead of % in crontab entries |
| Scripts in cron.daily skip | Filename contains a dot | Remove .sh extension on Debian-based systems |
| Email flood from cron | Verbose command output | Redirect output or set MAILTO="" |
See Also#
- Ansible-Bootstrap: declarative Linux hosts across any distro and hypervisor
- Terraform
- BackupDir.sh
- BackupMySQL.sh
- DeleteFilesOlderThan.sh