Skip to content

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 .service and a .timer;
  • write a cron-like schedule using OnCalendar=;
  • use interval (monotonic) timers — OnBootSec= and OnUnitActiveSec= — for schedules cron cannot express;
  • spread a fleet's load and control firing precision with RandomizedDelaySec= and AccuracySec=;
  • 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:

mytask.service   — what needs to run
mytask.timer     — when it needs to run

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

sudo nano /etc/systemd/system/log-cleanup.service
[Unit]
Description=Clean up old logs

[Service]
Type=oneshot
ExecStart=/usr/local/bin/log-cleanup.sh

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

sudo nano /etc/systemd/system/log-cleanup.timer
[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 (daily is 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:

systemd-analyze calendar 'Mon *-*-* 03:00:00'
  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

sudo systemctl daemon-reload
sudo systemctl enable --now log-cleanup.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

systemctl list-timers log-cleanup.timer
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

journalctl -u log-cleanup.service --since today

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:

systemctl status log-cleanup.service

Running It by Hand

To run the job immediately, without waiting for the schedule, for testing:

sudo systemctl start log-cleanup.service

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:

[Timer]
OnBootSec=1min
OnUnitActiveSec=10s

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:

[Timer]
OnCalendar=*-*-* 03:00:00
AccuracySec=1s

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:

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h

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 .service and forgetting the .timer. systemctl enable log-cleanup.service alone doesn't cause anything to run on a schedule — a Type=oneshot service only runs when its timer fires or it's started by hand. The unit that needs to be enabled is the .timer file.
  • Forgetting daemon-reload. After creating or editing a unit file, skipping systemctl daemon-reload leaves systemd working from its previous, stale view of the unit.
  • Confusing Persistent=true with cron's behavior. Persistent=true only 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 --now was omitted, or daemon-reload was skipped). If it is listed but NEXT shows n/a, the OnCalendar= expression is invalid — confirm it in isolation with systemd-analyze calendar '<expr>'. If the schedule is fine but the job still misbehaves, systemctl status <name>.timer shows unit-load errors, and journalctl -u <name>.service shows what happened the last time it actually fired.

Exercises

  1. Create a simple Type=oneshot service that runs echo/date, paired with a timer that fires every 5 minutes (OnCalendar=*:0/5).
  2. Watch the next scheduled run with systemctl list-timers, then confirm the output with journalctl -u.
  3. Check three different OnCalendar= expressions with systemd-analyze calendar and compare the "Next elapse" result against what you expected.
  4. 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.

References