Scheduled jobs run backups, rotate logs, renew certificates and clean temporary files. They also fail silently more often than any other part of a server. This post covers cron syntax, the reasons jobs fail under cron, and when a systemd timer is a better fit.
Versions in use on this date. Vixie-style cron 3.0 on Ubuntu 18.04.3 LTS and cronie 1.4 on CentOS 7.7; systemd 237 and 219 respectively. Timers exist on both, but OnCalendar features differ slightly between those systemd versions.
What actually runs a cron job
flowchart TB
CR[cron daemon wakes each minute] --> TB[Reads crontabs<br/>user, /etc/crontab, /etc/cron.d]
TB --> M{Time matches?}
M -->|yes| SH[Starts /bin/sh with minimal environment]
SH --> CMD[Your command]
CMD --> OUT[Output mailed to user<br/>or lost if no mail system]
CMD --> EX[Exit code ignored by cron]
M -->|no| W[Waits for next minute]
Cron does not log job output anywhere by default, and does not care whether the job succeeded. That is why you must add logging yourself.
Syntax
# m h dom mon dow command
0 2 * * * /usr/local/bin/backup.sh # every day at 02:00
*/15 * * * * /usr/local/bin/check.sh # every 15 minutes
30 6 * * 1-5 /usr/local/bin/report.sh # weekdays at 06:30
0 0 1 * * /usr/local/bin/monthly.sh # first of the month
@reboot /usr/local/bin/on-boot.sh
crontab -e # edit your own
crontab -l # list
sudo crontab -u www-data -l
System files (/etc/crontab, /etc/cron.d/*) have an extra user column before the command. Scripts dropped into /etc/cron.daily, /etc/cron.weekly etc. need no schedule but must be executable and have no dots in their names on Debian-family systems.
Why a job works in the terminal but not in cron
| Cause | Fix |
|---|---|
Minimal PATH (/usr/bin:/bin) |
Use absolute paths, or set PATH= at the top of the crontab |
| No profile or environment variables | Source what you need in the script |
% in the command is a newline |
Escape as \%, common in date +%F |
| Wrong user or no permission | Check the crontab owner; use sudo crontab -e for root |
| Different working directory | cd /path && ./script or use absolute paths |
| Script not executable or bad shebang | chmod +x and test env -i /bin/sh -c '/path/script' |
Log output and detect failures
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Check that cron ran the job: grep CRON /var/log/syslog (Ubuntu) or /var/log/cron (CentOS). Better, make the script report success, for example by touching a file or calling a monitoring “heartbeat” URL at the end, and alert when the heartbeat is missing. A job that never runs sends no error.
Prevent overlapping runs
If a job takes longer than its interval, copies pile up. Use flock:
*/5 * * * * flock -n /var/lock/sync.lock /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1
Stagger heavy jobs (not everything at 02:00) and add jitter on fleets so many servers do not hit a shared service at once.
systemd timers
A timer is a unit that activates a service on a schedule. You get journald logs, dependencies, resource limits and Persistent=true to run missed jobs after downtime.
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly
[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers
journalctl -u backup.service
systemd-analyze calendar "Mon..Fri 06:30" # check a schedule expression
| cron | systemd timer | |
|---|---|---|
| Setup | One line | Two unit files |
| Logs | Do it yourself | journald by default |
| Missed runs after downtime | Skipped (except anacron) | Persistent=true |
| Overlap control | flock |
Same unit is not started twice |
| Portability | Everywhere | systemd systems only |
Use cron for simple jobs and portability, and timers when you want logging, dependencies and catch-up behaviour.
Time zones
Cron uses the server’s local time zone. Check timedatectl; servers are usually best kept on UTC. Jobs scheduled during a daylight-saving change can run twice or be skipped, which is another reason to keep servers on UTC.