Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · Linux

Scheduling Jobs: cron and systemd Timers

Write reliable cron entries, understand why jobs that work in a terminal fail under cron, prevent overlapping runs, and compare cron with systemd timers.

4 min read

  • Ubuntu 18.04.3 LTS
  • cron 3.0
  • systemd 237
  • CentOS 7.7
  • cronie 1.4

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.