Choosing Between cron, systemd Timers, and at/batch for Scheduled Tasks on Unix‑like Systems
A decision guide comparing cron, systemd timers, and at/batch for scheduled tasks on Unix‑like systems, with a concrete systemd timer example and verification steps.
22 May 2026, 03:23 UTC

The scheduling problem
You need a recurring or one‑shot job to run reliably on a Unix‑like host. The three built‑in mechanisms—cron, systemd timers, and at/batch—overlap in purpose but differ in guarantees, portability, and operational visibility. Picking the wrong one leads to missed runs, silent failures, or extra maintenance burden.
Supported options at a glance
| Feature | cron | systemd timers | at / batch |
|---|---|---|---|
| Recurring schedules | Yes (crontab fields) | Yes (OnCalendar=, OnUnitActiveSec=) | No (one‑shot only) |
| Portability | All Unix‑like (BSD, macOS, minimal containers) | Linux with systemd ≥ 212 | Most Unix‑like (POSIX at) |
| Dependency ordering | None | Unit dependencies (After=, Requires=) | None |
| Missed‑run catch‑up | Only via anacron | Persistent=true | N/A |
| Resource control | None | CPUQuota, MemoryMax, IOWeight, etc. | None |
| Logging | Mail or manual redirect; syslog optional | Journal (per‑unit, structured) | Mail or manual redirect |
| Setup complexity | One line in crontab | Two unit files (timer + service) | One command (at or batch) |
| User‑level persistence | Works per‑user crontab | Requires loginctl enable-linger | Works per‑user |
When to pick each
- cron – Choose when you must run on non‑systemd platforms (FreeBSD, macOS, tiny containers) or when a single‑line schedule is sufficient and you can tolerate no built‑in dependency handling.
- systemd timers – Default on modern Linux (systemd ≥ 212). Use when you need calendar expressions richer than crontab (e.g.,
Mon..Fri 02:00), automatic catch‑up after downtime, per‑job resource limits, or structured logs. - at / batch – Reserve for one‑off deferred execution (
at 23:30) or load‑aware runs (batchstarts when the load average drops). They are not substitutes for recurring schedules.
Trade‑offs in detail
Reliability and visibility
systemd timers expose every run in the journal. journalctl -u myservice.service shows start time, exit code, and stdout/stderr. Cron writes to a mailer or to /var/log/cron only if the system logger is configured; otherwise output disappears silently.
Missed runs
With Persistent=true a timer that was inactive during a scheduled window fires immediately when the timer restarts. Cron has no equivalent unless you install and configure anacron, which adds another moving part.
Resource isolation
A systemd service unit can declare CPUQuota=20%, MemoryMax=200M, or Nice=10. Cron jobs inherit the caller’s limits and can starve the host.
Setup overhead
A timer requires two files (e.g., /etc/systemd/system/mytask.service and /etc/systemd/system/mytask.timer) and a systemctl daemon-reload. Cron needs only crontab -e. For a single static job the extra files are modest; for dozens of jobs the declarative approach scales better.
User sessions
User‑level timers only run while the user has an active session unless loginctl enable-linger <user> is set. This is a frequent oversight; cron user crontabs run regardless of login state.
Concrete validation with systemd timers
The following steps create a timer that runs every five minutes, verify the schedule, trigger a manual run, and inspect the journal. Perform them as root (or with sudo) unless you are using a lingering user session.
1. Create the service unit
# /etc/systemd/system/mytask.service
[Unit]
Description=Example recurring task
[Service]
Type=oneshot
ExecStart=/usr/local/bin/mytask.sh
# Optional resource limits
CPUQuota=10%
MemoryMax=100M
Make sure /usr/local/bin/mytask.sh is executable and writes a timestamp to a known location, e.g.:
#!/bin/sh
date +"%F %T" >> /var/log/mytask.log
2. Create the timer unit
# /etc/systemd/system/mytask.timer
[Unit]
Description=Run mytask every 5 minutes
[Timer]
OnCalendar=*:0/5
Persistent=true
RandomizedDelaySec=30
[Install]
WantedBy=timers.target
OnCalendar=*:0/5means “every hour at minutes 0,5,10,…,55”.Persistent=trueenables catch‑up for missed windows.RandomizedDelaySec=30spreads the start up to 30 seconds to avoid thundering‑herd spikes.
3. Reload and enable
systemctl daemon-reload
systemctl enable --now mytask.timer
4. Verify the next fire times
systemctl list-timers --all | grep mytask
You should see a line with NEXT and LAST columns reflecting the five‑minute cadence.
5. Check the parsed calendar expression (optional but recommended)
systemd-analyze calendar '*:0/5'
The output lists the next few elapse times; confirm they match expectations before deploying.
6. Trigger a manual run
systemctl start mytask.service
This runs the service immediately, independent of the timer.
7. Inspect the journal
journalctl -u mytask.service -n 20 --no-pager
Look for the Started Example recurring task line, the script’s stdout (the timestamp), and a final Main PID=… code=exited, status=0/SUCCESS.
Verification checklist
- Timer schedule –
systemctl list-timers --allshows correctNEXT. - Calendar syntax –
systemd-analyze calendarreturns expected timestamps. - Manual execution –
systemctl start mytask.servicewrites to/var/log/mytask.log. - Journal entry –
journalctl -u mytask.serviceshows exit status 0. - Missed‑run catch‑up – Stop the timer (
systemctl stop mytask.timer), wait past a scheduled minute, thensystemctl start mytask.timer; the service should fire once immediately ifPersistent=true.
Limitations and gotchas
- systemd timer calendar syntax changed between releases; always test with
systemd-analyze calendaron the target host. - Cron discards stdout/stderr unless a mailer (e.g.,
postfix) is configured or you redirect to a file (> /var/log/mycron.log 2>&1). - User timers need
loginctl enable-linger <user>(run as root) to survive logout. atandbatchrely on theatddaemon; on some minimal images it is not installed.- Resource limits in the service unit only apply when the service runs under systemd; a script invoked via
ExecStartPre=orExecStartPost=inherits the same slice.
Practical way to check the result
After the first automated run, examine /var/log/mytask.log. It should contain a line for each five‑minute interval. If gaps appear, cross‑reference journalctl -u mytask.timer for timer activation events and journalctl -u mytask.service for service outcomes. This two‑log correlation is the quickest proof that the timer behaves as declared.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.