systemd timers instead of cron: when the switch is right
Where systemd timers genuinely beat cron: dependencies, catch-up runs, journal logging, per-unit hardening, weighed honestly.
Cron runs everywhere, is well understood, and will remain so. Still, every larger environment eventually asks whether systemd timers have become the better choice. Our honest answer: it depends, and it depends in a way you can reason about. This post covers the differences that actually matter in operations, the cases where we build the timer variant, and the cases where a classic crontab stays the boring right answer.
The catch-up run is the real difference
The most quoted timer feature is catching up missed schedules. It rests on a detail that separates the two worlds: cron decides at wake-up time whether a slot has passed; the missed run is simply lost. A timer with Persistent=true records when it last fired and fires at the next boot if the schedule fell inside downtime. For maintenance jobs tied to a night window that is a real difference: a server that reboots itself at 03:00 runs its backup routine at startup instead of waiting for the next night.
Where catch-up is unwanted, dropping Persistent still leaves OnCalendar, a schedule syntax that reads like a crontab line but with far more expressive range: month lists, time ranges, week-of-month combinations and dynamic points such as daily~ for calendar events with catch-up semantics.
Dependencies and surroundings cron never had
The second comparison is integration with the rest of the system. A timer lives in the systemd world, which concretely means: the paired service gets Requires= and After= relationships, resource control through MemoryMax and CPUQuota, its own sandboxing set with ProtectSystem, PrivateTmp and friends, and complete logging into the journal. A cron job runs in an environment that varies between distributions, logs through a detour into syslog, and encapsulates its environment in a syntax everyone has misguessed once, because % characters carry their own meaning inside a crontab line.
journalctl -u <service> is especially practical when auditing a run: who started what, when, why, and what the output was, in one query, without first constructing stderr redirections.
Where cron still wins
The positions where we leave cron in place are plain and honest: first, when people maintain the lines with traditional tooling and the team wants to keep crontab fluency alive; second, for user-level tasks that take one minute with crontab -e while their systemd counterparts require a user-unit setup that the task does not justify; third, where external tooling expects its rule files in /etc/cron.d/, such as software that ships its own schedules. None of these reasons is deep, but they cost less than the timer advantages are worth in exactly that niche. In operations, the solution the team reliably maintains beats the elegant one.
Migrating in small steps
A migration does not need to be a project. Our scheme: for each cron entry, write a timer unit and a service unit, trigger the run once manually with systemctl start, inspect the journal output, then verify OnCalendar with systemd-analyze calendar to make sure the pattern expresses the intended times. The cron line stays commented out in its file until a review cycle confirms nothing is missing. Removal happens only after a full observation period, for example a month covering all relevant schedules.
One boundary worth stating explicitly: timers solve the problem "hold a schedule reliably and log it well" better than cron does, but no scheduler solves the problem "job that runs too long". Timeouts in the unit and a lock-file discipline against overlapping runs remain part of the routine regardless of the scheduler.
What we set by default
Watching timers across the fleet
The switch comes with a control room cron never had: systemctl list-timers shows at one glance which schedules are planned, when they last ran, and when they come due again. That makes a short weekly routine possible: scan the list once, watch for units that persistently show a next run but never fire, and watch for times parked unexpectedly far in the future. Both symptoms usually mean a unit that has never succeeded, and the silence of its absence is the damage.
The second observation point is the protocol of failed starts. A timer whose service has failed three times in a row stays invisible until its next calendar slot; a short script that reports unit failures into the weekly overview turns that into a visible task.
On the servers we manage the line is simple: new recurring jobs get timers, existing cron entries migrate opportunistically, and more complex orchestration moves into purpose-built units with explicit ordering. The intent behind it is unspectacular: logging in the same place as the rest of the system, restart behavior consistent with systemd operation, and a job definition in a file that goes into version control with everything else. Beyond that it is routine you stop noticing once it is set up, which is exactly what a good scheduler should achieve. If you are structuring maintenance jobs across a fleet, our Linux administration page describes the service behind it, and our post on zero-downtime nginx deployment with systemd shows units working together in production.
Sources
- freedesktop.org, systemd.timer(5), https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html (accessed 2026-11-04)
- freedesktop.org, systemd.service(5), https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html (accessed 2026-11-04)
- freedesktop.org, systemd.time(7), https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html (accessed 2026-11-04)