Skip to content
Linux Administration
Lab 19 of 27·35mIntermediate

Schedule work with systemd timers, and with cron

Run the same job both ways, then compare what each one tells you when it fails — which is the only reason to prefer one.

You need

  • A Linux VM with systemd and sudo access

Do first

Cron is everywhere and fine. Timers are better at exactly one thing that matters at 3am: telling you what happened. Do both, then decide with evidence.

1. A job worth scheduling

sudo tee /usr/local/bin/diskreport.sh >/dev/null <<'SCRIPT'
#!/bin/bash
set -euo pipefail
USED=$(df --output=pcent / | tail -n 1 | tr -dc '0-9')
echo "root filesystem ${USED}% used"
[[ $USED -lt 90 ]] || { echo "over threshold" >&2; exit 1; }
SCRIPT
sudo chmod 755 /usr/local/bin/diskreport.sh
/usr/local/bin/diskreport.sh; echo "exit=$?"

It prints a line and exits non-zero over 90%. Both matter: a scheduler needs an exit status to decide whether something failed.

Verify

/usr/local/bin/diskreport.sh # root filesystem NN% used

2. Schedule it with cron

sudo tee /etc/cron.d/diskreport >/dev/null <<'CRON'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/5 * * * * root /usr/local/bin/diskreport.sh
CRON
sudo chmod 644 /etc/cron.d/diskreport
sudo systemctl restart cron 2>/dev/null || sudo systemctl restart crond

Two things bite people here. A file in /etc/cron.d needs a user field (root above) that a personal crontab does not, and it must not be world-writable or cron ignores it. And cron runs with a nearly empty environment — setting PATH explicitly is not optional, because the script that works in your shell fails under cron for want of it.

Verify

sudo run-parts --test /etc/cron.d 2>/dev/null; ls -l /etc/cron.d/diskreport # -rw-r--r-- 1 root root ... /etc/cron.d/diskreport

3. Schedule it with a timer

sudo tee /etc/systemd/system/diskreport.service >/dev/null <<'UNIT'
[Unit]
Description=Disk usage report

[Service]
Type=oneshot
ExecStart=/usr/local/bin/diskreport.sh
UNIT

sudo tee /etc/systemd/system/diskreport.timer >/dev/null <<'UNIT'
[Unit]
Description=Run the disk usage report every 5 minutes

[Timer]
OnCalendar=*:0/5
Persistent=true
AccuracySec=30s
RandomizedDelaySec=15s

[Install]
WantedBy=timers.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now diskreport.timer
systemctl list-timers diskreport.timer --no-pager

A timer is two units: a .service describing the work as Type=oneshot, and a .timer describing when. Four directives with no cron equivalent:

  • Persistent=true runs a missed job after downtime. Cron simply skips it.
  • AccuracySec lets systemd batch wakeups instead of firing on the second, which matters on a laptop or a dense host.
  • RandomizedDelaySec jitters the start, so a hundred hosts do not hammer the same endpoint at exactly midnight.
  • OnCalendar takes readable expressions: daily, Mon..Fri 09:00, *:0/5.

Verify

systemctl is-active diskreport.timer # active

4. Check the calendar expression before trusting it

systemd-analyze calendar "*:0/5"
systemd-analyze calendar "Mon..Fri 09:00"
systemd-analyze calendar "daily"

This prints the normalised form and the next few firings. There is no cron equivalent — the standard way to check a cron expression is to wait and see, which is how a job meant for 2am ran every minute for a week.

Verify

systemd-analyze calendar "*:0/5" | grep -c "Next elapse" # 1

5. Now compare what each tells you after a failure

Force the job to fail:

sudo tee /usr/local/bin/diskreport.sh >/dev/null <<'SCRIPT'
#!/bin/bash
set -euo pipefail
echo "about to fail"
exit 3
SCRIPT
sudo chmod 755 /usr/local/bin/diskreport.sh
sudo systemctl start diskreport.service

The timer's side:

systemctl status diskreport.service --no-pager | head -n 8
journalctl -u diskreport.service -n 15 --no-pager
systemctl show diskreport.service -p ExecMainStatus --value

You get the exit status, the output, a timestamp, and a unit state you can alert on. Cron's side, by contrast, mails output to the local user if a mail transport exists — and on a cloud instance one usually does not, so the output goes nowhere:

journalctl -t CRON -n 10 --no-pager | tail -n 5
ls -l /var/mail/root 2>/dev/null || echo "no local mail spool — cron output is lost"

That is the whole argument. Cron schedules reliably and reports badly; a timer's failure is a queryable unit state.

Verify

systemctl show diskreport.service -p ExecMainStatus --value # 3

Which to use

Use a timer when the job matters: you want the exit status, the output in the journal, Persistent catch-up, and jitter across a fleet. Use cron when the box has no systemd, when the job is trivial and self-logging, or when a runbook you cannot change already says cron. Do not run both for the same job, which this lab did only to compare them.

Clean up

sudo systemctl disable --now diskreport.timer
sudo rm -f /etc/systemd/system/diskreport.timer \
  /etc/systemd/system/diskreport.service
sudo rm -f /etc/cron.d/diskreport /usr/local/bin/diskreport.sh
sudo systemctl daemon-reload
sudo systemctl restart cron 2>/dev/null || sudo systemctl restart crond

Where this goes next

You can schedule work and find out how it went. The cloud and DevOps group is next, and it opens with the thing scheduled jobs most often exist to do: a backup you have actually restored.