Why cron
Run a command at N o'clock every day, every 5 minutes, or on the first of each month. Forty years a Unix standard. Simple format, the daemon is always present, nothing to write beyond a schedule line.
On modern Linux, systemd-timers is increasingly the replacement. cron is still installed on RHEL, Debian, and Alpine out of the box and continues to serve millions of servers.
crontab: where the schedule lives
Three levels:
| File | Who writes it | user field |
|---|---|---|
crontab -e (per-user) | the user, via crontab | absent |
/etc/crontab | root | present (6th field) |
/etc/cron.d/* | packages, root | present |
/etc/cron.{hourly,daily,weekly,monthly}/ | scripts | run-parts |
Personal crontab for the current user:
crontab -e # edit
crontab -l # list
crontab -r # delete entire crontab (careful!)
sudo crontab -u www-data -l # another user's crontab
System files under /etc/cron.d/ use a 6-field format: the username
comes before the command:
# m h dom mon dow user command
0 3 * * * root /usr/local/bin/backup.sh
Line format
┌─── minute (0-59)
│ ┌─── hour (0-23)
│ │ ┌─── day of month (1-31)
│ │ │ ┌─── month (1-12 or JAN-DEC)
│ │ │ │ ┌─── day of week (0-7, 0 and 7 = Sunday, or MON-SUN)
│ │ │ │ │
* * * * * command
Special characters:
| Character | Meaning |
|---|---|
* | every value |
, | list: 1,15,30 |
- | range: 9-17 |
*/N | every N: */5 every 5 |
0-23/2 | range with step |
Examples:
*/5 * * * * ./tick.sh # every 5 minutes
0 3 * * * ./backup.sh # every day at 03:00
0 2 * * 0 ./weekly.sh # every Sunday at 02:00
0 0 1 * * ./monthly.sh # 1st of the month at midnight
30 9-18 * * 1-5 ./reminder.sh # at :30 past each hour from 09:30 to 18:30, Mon-Fri
@ shortcuts
| Alias | Equivalent |
|---|---|
@reboot | when cron starts (roughly at system boot) |
@yearly, @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily, @midnight | 0 0 * * * |
@hourly | 0 * * * * |
@reboot is a quick substitute for a systemd unit when you need a
one-shot command at boot. For reliability, systemd-targets is the
better choice.
anacron: for machines that are powered off
cron will not run a job that was due while the machine was down. anacron catches up: at boot it checks when each job last ran and executes it now if it is overdue.
/etc/anacrontab:
# period delay job-id command
1 5 cron.daily run-parts /etc/cron.daily
7 25 cron.weekly run-parts /etc/cron.weekly
period: how many days between runsdelay: random delay after startup, in minutes
On laptops where @daily jobs are missed during sleep, anacron runs
them on the next boot.
The cron environment: the source of 80% of bugs
cron launches commands in a minimal environment: PATH=/usr/bin:/bin,
HOME set to the user's home directory, and none of the variables from
~/.bashrc or /etc/profile. A script that works fine in the shell
often fails in cron for this reason.
The fix:
# Set variables at the top of the crontab
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com
TZ=Europe/Berlin
0 3 * * * /home/user/backup.sh
PATH: the most common cause of "command not found"MAILTO: cron mails stdout/stderr to this address (requires an MTA)SHELL: defaults to/bin/sh; set to bash if the script uses bash-only syntaxTZ: cron uses the system zone; on a UTC server "0 3" means 03:00 UTC
Logs
journalctl -u cron -f # systemd systems (Debian)
journalctl -u crond -f # RHEL
grep CRON /var/log/syslog # older Debian
/var/log/cron # RHEL
cron logs the fact that it ran a job, not the stdout or stderr. To
see output, use MAILTO or redirect in the job itself:
0 3 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1
Without 2>&1, errors go to mail rather than the log file. A cleaner
approach sends everything to syslog with a tag:
0 3 * * * /home/user/backup.sh 2>&1 | logger -t backup
The output then appears in journalctl -t backup with timestamps.
cron vs systemd timers
| Feature | cron | systemd-timers |
|---|---|---|
| Format | one-line schedule | .timer + .service unit |
| Logs | syslog, fact-of-run only | full stdout/stderr via journald |
| Dependencies | none | After/Requires |
| Random delay | anacron | RandomizedDelaySec= |
| Persistence | anacron | Persistent=true |
| Missed jobs after sleep | anacron | OnBootSec= |
| Long-running process | unsupervised | resource limits, cgroups |
For new code on systemd systems, timers are the more complete solution.
Troubleshooting
command not found:PATHdoes not include/usr/local/bin. Set it in the crontab.- Script cannot find a file: cron runs in the user's
$HOME. Use absolute paths, or putcdas the first line of the script. - Job never runs: check
systemctl status cron,crontab -l(syntax), and the script permissions (chmod +x). - Job runs but silently fails:
MAILTOis unset and stdout/stderr has no redirect. Redirect output to a file and read it. - Job runs twice: the same entry exists in both
/etc/crontaband the personal crontab. - Clock change, DST: at transition moments a job may run zero or two times. Use anacron-style scheduling and make jobs idempotent.
%in a crontab command:%is a special character in crontab; escape it as\%or wrap the command in/bin/bash -c '...'.