Skip to content
LinuxHow-To Published Updated 5 min readViews unavailable

How to Replace a Cron Job with a systemd Timer

A complete, working example converting a nightly backup cron job into a properly supervised systemd timer, with logging and failure visibility cron never gave you.

This converts a typical cron job — a nightly backup script — into an equivalent systemd timer, gaining proper logging, dependency ordering, and failure visibility that a bare cron entry doesn’t provide.

Step 1: the cron job you’re replacing

Assume you currently have this in root’s crontab:

0 2 * * * /usr/local/bin/backup.sh

Step 2: create the service unit

A systemd timer always pairs with a service unit describing what to actually run:

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup job
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

Type=oneshot tells systemd this service runs to completion and exits, rather than staying resident — the right type for a script that does its job and finishes, as opposed to a long-running daemon.

Step 3: create the timer unit

# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup.service nightly at 2am

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true is the single biggest practical improvement over cron here — if the machine was powered off or suspended at 2am, the timer fires as soon as the system is next running, rather than silently skipping that day’s backup entirely the way cron would.

Step 4: enable and start the timer

systemctl daemon-reload
systemctl enable --now backup.timer

Note you enable and start the timer, not the service directly — the service only runs when the timer fires (or when triggered manually).

Step 5: verify the timer is scheduled correctly

systemctl list-timers backup.timer
NEXT                        LEFT     LAST  PASSED  UNIT          ACTIVATES
Tue 2026-01-13 02:00:00 UTC  6h left  n/a   n/a     backup.timer  backup.service

Step 6: test it manually before waiting for 2am

systemctl start backup.service
systemctl status backup.service

Starting the service directly runs it immediately, independent of the timer’s schedule — the right way to test your script’s systemd integration without waiting for the actual scheduled time.

Step 7: check logs — the actual upgrade over cron

journalctl -u backup.service --since today

Every run’s output — stdout, stderr, exit status, and timing — is captured automatically in the systemd journal, queryable by time range, without needing to configure MAILTO or redirect output to a log file manually the way a cron job typically requires.

Step 8: get alerted on failure, not just logged

# add to backup.service's [Unit] section
OnFailure=notify-failure@%n.service

Pairing a failure with a notification unit (a small separate service that sends an alert, however you prefer to be notified) turns a silent cron failure into an actual, actionable alert — something cron has no native mechanism for at all.

Why this is worth the extra setup over a one-line crontab entry

A cron job that silently fails produces no signal at all unless you happen to notice a backup is missing — its output only goes anywhere if you’ve separately configured mail delivery, and a missed run (system was off) is simply skipped forever with no record. The systemd timer equivalent gives you queryable structured logs, Persistent=true catch-up semantics, and a natural hook for failure notifications, all using infrastructure already running on any modern systemd-based Linux system.

Step 9: avoid a thundering herd across many identically-configured machines

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=1800
Persistent=true

If the same timer definition is deployed identically across many machines (a fleet of servers all running the same nightly job), every instance firing at the exact same wall-clock second can produce a real, simultaneous spike in load against whatever shared resource the job touches — a backup target, a database, a network link. RandomizedDelaySec adds a random delay, bounded by the given value, independently chosen per machine on each activation, spreading otherwise-simultaneous fleet-wide runs across a window rather than all landing in the same instant. Cron has no equivalent built-in mechanism at all for this — achieving the same spread with cron requires hand-writing a random sleep into the job script itself.

Step 10: monotonic timers, for “run relative to an event” rather than a fixed clock time

[Timer]
OnBootSec=15min
OnUnitActiveSec=1h

OnCalendar (used above for the nightly backup) fires at specific wall-clock times — the other timer family, monotonic timers, fires relative to an event instead: OnBootSec counts from system boot, OnUnitActiveSec counts from the last time the paired service started, letting you express “run 15 minutes after boot, then every hour thereafter” without hardcoding any actual clock time at all. This is the more natural fit for a periodic task that genuinely doesn’t care what time of day it runs, only that it runs on a consistent interval relative to the system’s own uptime or the job’s own prior runs — a distinction cron’s single, purely calendar-based syntax has no equivalent way to express directly. Both timer types can even be combined in the same [Timer] section, giving a job that runs shortly after every boot and additionally on its own regular calendar schedule thereafter — a combination with no clean single-line cron equivalent at all, since cron has no concept of “relative to boot” scheduling whatsoever and would need a separate @reboot entry plus a completely independent regular entry to approximate the same behavior, without the shared context a single systemd timer unit naturally provides between the two triggers.

Related:

Sources:

Comments