Cron vs systemd timers

A timer costs two files instead of one line and buys four things cron has no answer for: logging, missed-run catch-up, arbitrary intervals, and dependencies.

Method note: the cron behaviour below was computed with the parser on this site. systemd is not available on the machine these guides are written on, so the timer syntax and semantics follow the systemd.timer and systemd.time manual pages rather than a live run — no output is quoted as observed.

The same job, both ways

# cron — one line
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

# systemd — two files
# /etc/systemd/system/backup.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

For this job the crontab line is plainly better: shorter, portable, and obvious to anyone who reads it. The timer earns its extra weight only when you need one of the capabilities below.

1. Intervals cron cannot express

# "every 90 minutes" in cron: no such expression

*/45 * * * *  from 00:50 ->  01:00 | 01:45 | 02:00 | 02:45
                                  └─45m─┘ └15m┘ └─45m─┘

# systemd
OnUnitActiveSec=90min

Cron steps divide a field's range, and the minute range restarts every hour, so nothing spans more than sixty minutes. We measured */45 producing alternating gaps of 45 and 15 minutes — the detail is in cron every 5 minutes.

OnUnitActiveSec measures from the previous run rather than against a clock face, so the interval is exactly what it says.

2. Missed runs

Persistent=true records when the timer last fired and runs a missed occurrence as soon as the machine is available again. Cron has no equivalent: an occurrence that passes while the host was off is simply gone, which is why anacron exists as a separate tool for laptops and intermittently-running machines.

On a server that stays up this rarely matters. On anything that reboots, sleeps or scales down, it is the difference between a nightly job running and quietly not.

3. Output you can query

systemctl list-timers          # next run, time remaining, last run
journalctl -u backup.service   # output and exit status of past runs

Cron's default is to email output, which on most modern servers goes nowhere — so people redirect to a file and then have to remember which file. And nothing in cron tells you when a job will next run; you read the expression and work it out, which is why tools that compute run times exist at all.

This is the difference most people notice first after switching, ahead of any scheduling feature.

4. It is a service, with everything that implies

A timer triggers a service unit, so the job inherits the rest of systemd's vocabulary: After= to wait for a database, MemoryMax= to bound it, User= without a su wrapper, and a restart policy. In cron all of that has to be built inside the script.

Choosing

SituationUse
Fixed schedule, host always on, output to a logcron
Portability across Unixes matterscron
Interval that does not divide an hourtimer
Host reboots, sleeps or scales downtimer
Job needs dependencies or resource limitstimer
You want to ask when it next runstimer

One thing both share: daylight saving. OnCalendar accepts an explicit timezone the way CRON_TZ does, and the same advice applies — keep schedules out of the 01:00–03:00 local window.

Frequently asked questions

What does a systemd timer give me that cron does not?

Four things cron has no answer for. Output goes to the journal automatically, so a failing job is queryable rather than emailed into a void. Persistent=true runs a job that was missed while the machine was off, which cron simply skips. OnUnitActiveSec expresses arbitrary intervals such as every 90 minutes, which cron cannot state because no field spans more than an hour. And the timer triggers a service unit, so it inherits dependencies, resource limits, a User setting and a restart policy — all of which would otherwise live inside the script. The cost is two files and a different mental model for something a crontab does in one line, so the timer has to earn that overhead with one of the four, not with novelty.

Why does every 90 minutes not work in cron?

Because cron steps divide a field's range and the minute range restarts every hour, so no expression spans more than sixty minutes. The nearest approximations are wrong in a way that is easy to miss — */45 fires at :00 and :45, giving alternating gaps of 45 and 15 minutes rather than a steady 45. A systemd timer states the period directly with OnUnitActiveSec=90min and measures from the last run rather than against a clock face, so the gap is the number you wrote. This is the clearest case where the extra files buy something cron cannot express at all, rather than merely expressing it more verbosely than a crontab line.

What is Persistent=true?

It tells the timer to run a missed job as soon as the machine is available again. systemd records when each timer last fired, so if a nightly task was scheduled while the server was powered off or rebooting, it runs on the next boot instead of being skipped. Standard cron has no equivalent — a missed occurrence is simply gone, which is why anacron exists as a separate tool for laptops and machines that are not continuously powered. If your job matters and the host is not continuously up, this alone is a reason to prefer a timer, since a silently skipped nightly job is indistinguishable from one that never existed until someone checks.

Is cron still the right choice for anything?

Yes, for the majority of simple recurring jobs on a machine that stays on. A crontab line is one line, editable with crontab -e, portable to any Unix, and understood by everyone who will ever read it. A timer is two files, a systemctl enable, and knowledge of unit syntax. When the job is a script that runs at a fixed time and logs to a file you already tail, the extra machinery buys nothing. Reach for a timer when you need one of its four specific capabilities, not by default, and not because timers are newer than crontabs.

How do I see when a timer will next run?

systemctl list-timers shows every active timer with its next elapse time, the time remaining, and when it last fired — a view cron has no equivalent of at all. With cron you read the crontab and work the schedule out yourself, which is why tools that compute run times exist. Pair that with journalctl -u yourjob.service to read the output of previous runs, including exit status. This observability gap is the difference most people notice first after switching, more than any of the scheduling features, because it changes how you debug rather than how you schedule, which you do far more often.

References

Read a cron expression with the parser, or start from crontab syntax. More tools at withuse.io/tools.