systemd Timers: An Alternative to cron
cron Basics and crontab and System cron covered cron, a simple and reliable tool that has been in use for decades. On a system where systemd is already running as PID 1, there's an alternative: the systemd timer. It performs the same job as cron — running a command on a schedule — but does it as a systemd unit, integrated with journalctl, unit dependencies, and resource controls.
This article isn't written to argue cron is "wrong" — it lays out the practical differences between the two so a real choice can be made between them.
What You Will Learn
By the end of this article you will be able to:
- explain that a systemd timer is made of two files — a
.serviceand a.timer; - write a cron-like schedule using
OnCalendar=; - use interval (monotonic) timers —
OnBootSec=andOnUnitActiveSec=— for schedules cron cannot express; - spread a fleet's load and control firing precision with
RandomizedDelaySec=andAccuracySec=; - enable a new timer and check its status and next run time;
- view a timer's output through
journalctl, and diagnose a timer that never fires; - decide when cron is the better choice and when a systemd timer is.
Two Files Instead of One Line
In cron, the schedule and the command are written together on one line. A systemd timer, by contrast, is made of two separate unit files:
The advantage of this separation: mytask.service can be started by hand, independently of the schedule, at any time (systemctl start mytask.service) — convenient for testing or for a manual re-run.
Example: Daily Log Cleanup
1. Create the service file
Type=oneshot tells systemd this unit isn't a continuously running service but a task that starts once and finishes. A unit of this type is not normally started on its own — it's meant to be triggered by a timer.
2. Create the timer file
[Unit]
Description=Daily schedule for log-cleanup.service
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
OnCalendar=daily— run once a day at 00:00 (dailyis shorthand for*-*-* 00:00:00);Persistent=true— if the server was off, or the timer's scheduled moment was missed for any reason, the missed run is triggered as soon as the system comes back up (cron has no equivalent behavior).
OnCalendar= uses a more readable syntax than a cron expression:
| Goal | OnCalendar= value |
Rough cron equivalent |
|---|---|---|
| Every day at midnight | daily or *-*-* 00:00:00 |
0 0 * * * |
| Every hour | hourly |
0 * * * * |
| Every Monday at 03:00 | Mon *-*-* 03:00:00 |
0 3 * * 1 |
| The 1st of every month at 04:30 | *-*-01 04:30:00 |
30 4 1 * * |
An OnCalendar= expression can be checked before it's ever wired into a real timer:
Original form: Mon *-*-* 03:00:00
Normalized form: Mon *-*-* 03:00:00
Next elapse: Mon 2026-07-27 03:00:00 UTC
Next elapse confirms the expression actually resolves to the intended time, without needing to reboot or wait for it to fire.
3. Enable the timer
--now both activates the timer immediately and ensures it starts automatically at boot. Note what's being enabled here — log-cleanup.timer, not log-cleanup.service. The service doesn't need to be enabled directly; the timer is what triggers it.
Checking Status
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sat 2026-07-25 00:00:00 UTC 13h left n/a n/a log-cleanup.timer log-cleanup.service
- NEXT/LEFT — the next scheduled run and how long until it fires;
- ACTIVATES — which service the timer is set to trigger.
This is a clear advantage systemd timers have over cron: the next scheduled run time can be checked directly, rather than estimated by reading a crontab line or waiting for a reboot.
Viewing Output: journalctl
In cron, output is normally tracked through an email that has to be configured separately, or a manually chosen log file (covered in cron Environment and Logs). With a systemd timer, everything log-cleanup.service writes to stdout or stderr lands in journalctl automatically, with no additional configuration.
Whether the job finished successfully or with an error:
Running It by Hand
To run the job immediately, without waiting for the schedule, for testing:
This doesn't touch the timer at all — it just runs the service once, letting you test the underlying command without temporarily changing the schedule.
Interval Timers: OnBootSec and OnUnitActiveSec
OnCalendar= pins a job to wall-clock times, exactly as cron does. systemd also offers monotonic timers that fire relative to an event rather than the calendar:
OnBootSec=— a fixed delay after the machine boots, e.g.OnBootSec=15min("15 minutes after every boot");OnUnitActiveSec=— a fixed interval measured from the last time the service was activated, e.g.OnUnitActiveSec=10s.
The two are usually combined so the first run has a defined starting point:
This means "first run a minute after boot, then every 10 seconds thereafter." That schedule has no cron equivalent at all — cron can only express wall-clock times and its finest resolution is one minute. A job that must run every 10 seconds, or "N minutes after this host booted," is precisely the case the cron Basics granularity note pointed here for: use a timer, because cron cannot express it.
Precision and Load Spreading: AccuracySec and RandomizedDelaySec
By default a timer does not fire at the exact second requested. AccuracySec= defaults to 1 minute, so systemd may delay a job by up to a minute to batch it with other wakeups and save power. A job that genuinely needs to fire on the second must ask for it:
RandomizedDelaySec= does the opposite, and is a real production advantage over cron — it adds a random offset, up to the value given, to each firing:
On a fleet of a hundred servers all running the same daily backup, a plain 0 0 * * * cron line makes every host hit the backup target at midnight simultaneously — a self-inflicted thundering herd on the network and the destination. RandomizedDelaySec=1h scatters those hundred runs randomly across the hour after midnight, with no coordination needed between hosts. Achieving the same with cron means hand-staggering each host's schedule one by one.
cron vs. systemd Timer: When to Choose Which
| Criterion | cron | systemd timer |
|---|---|---|
| Syntax simplicity | One line, quick to write | Two files, more boilerplate |
| Tracking output and errors | Requires separate setup (email, log file) | Automatic, through journalctl |
| Re-running a missed job (system was off) | No | Yes, with Persistent=true |
| Previewing the next run time | Hard | Easy, with systemctl list-timers |
| Depending on another systemd unit (e.g., after the network is up) | No | Yes, with Wants=, After= |
| Containerized or minimal environments | Often already installed | Requires systemd (missing in some containers) |
| Familiarity and documentation | Extremely widespread, recognized almost everywhere | Common on systemd-based systems, but not as universal as cron |
In practice, either can be the right answer: on an existing, systemd-based server, a timer is more convenient for more complex jobs that need observability or dependencies; for simple, quickly written jobs that might need to move to a different kind of system later, cron remains a practical choice.
Common Mistakes
- Enabling the
.serviceand forgetting the.timer.systemctl enable log-cleanup.servicealone doesn't cause anything to run on a schedule — aType=oneshotservice only runs when its timer fires or it's started by hand. The unit that needs to be enabled is the.timerfile. - Forgetting
daemon-reload. After creating or editing a unit file, skippingsystemctl daemon-reloadleaves systemd working from its previous, stale view of the unit. - Confusing
Persistent=truewith cron's behavior.Persistent=trueonly makes up for a run missed while the server was powered off. cron has no equivalent setting at all — a job scheduled during downtime is simply skipped and never runs. - A timer that never fires, with no obvious error. Diagnose it in order: run
systemctl list-timers --all— if the timer isn't listed at all, it was never enabled/started (the--nowwas omitted, ordaemon-reloadwas skipped). If it is listed butNEXTshowsn/a, theOnCalendar=expression is invalid — confirm it in isolation withsystemd-analyze calendar '<expr>'. If the schedule is fine but the job still misbehaves,systemctl status <name>.timershows unit-load errors, andjournalctl -u <name>.serviceshows what happened the last time it actually fired.
Exercises
- Create a simple
Type=oneshotservice that runsecho/date, paired with a timer that fires every 5 minutes (OnCalendar=*:0/5). - Watch the next scheduled run with
systemctl list-timers, then confirm the output withjournalctl -u. - Check three different
OnCalendar=expressions withsystemd-analyze calendarand compare the "Next elapse" result against what you expected. - Write the same job first as a cron line, then as a systemd timer, and compare how much setup each one required.
Summary
A systemd timer serves the same purpose as cron — running a command on a schedule — but does it through a separate .service/.timer file pair, automatic logging via journalctl, and extras like Persistent=true. For simple, quickly written jobs, cron remains practical; for jobs that need closer observability or unit dependencies, a systemd timer gives more control.