Developer guide

Systemd Timers vs Cron: When to Switch and How to Migrate

Your crontab has worked for years — but systemd timers give you missed-run recovery withPersistent=true, first-class logging with journalctl, and a sandbox for every job. This guide compares the two across 5 practical dimensions and walks you through migrating a real crontab entry, OnCalendar by OnCalendar.

Benjamin Rotshtein

Written by Benjamin Rotshtein

Updated

Should I replace cron with systemd timers?

For most Linux servers, yes — systemd timers give you missed-run recovery with Persistent=true, built-in journald logging, and dependency control, with only slightly more verbose unit files. Cron is still fine for simple schedules or where timers are unavailable. This guide compares both in 5 practical dimensions with ready-to-use examples.

Should I switch from cron to systemd timers?
Yes, if the machine runs systemd and you want missed-run recovery, easy logging or per-job hardening. Keep cron on legacy distros or when you rely on cron-only behavior.
What is the key syntax difference?
Cron uses 5 cryptic fields; systemd uses OnCalendar=daily, Mon..Fri 09:00:00 and similar readable expressions.
How do I avoid missing runs after downtime?
Set Persistent=true on the timer so a missed schedule runs at the next boot.

Why systemd timers beat cron for most jobs

Classic cron has a minimal environment, no dependency ordering and no concept of a “missed” run — if the daemon or the machine was off, the job simply does not fire. Systemd timers run your job as a unit, so you get the whole unit ecosystem: journald logs, ordering, sandboxing and dependency handling for free.

The comparison table

Dimensioncronsystemd timers
Schedule syntax5 fields (minute hour dom month dow)OnCalendar, written in plain terms (daily, Mon..Fri 09:00:00)
Missed runsSkipped silently if the daemon was downPersistent=true replays missed runs at next boot
LoggingSyslog or wherever you redirect outputBuilt-in journald with journalctl -u <unit>
DependenciesNo ordering; fires when the time matchesRequires/After ordering against other units
Security hardeningNone built inSandbox directives (ProtectSystem, NoNewPrivileges, PrivateTmp)
EnvironmentNear-empty PATH; you must export everythingEnvironment=/EnvironmentFile= supported natively
Managementcrontab -e per usersystemctl enable/start/status per unit

OnCalendar examples you can copy

Each line below is a valid OnCalendar= value. Verify any of them with systemd-analyze calendar 'daily' — it prints the next runs so you never guess again.

OnCalendar valueMeaning
OnCalendar=dailyEvery day at 00:00:00
OnCalendar=*-*-* 09:00:00Every day at 09:00:00
OnCalendar=Mon..Fri 09:00:00Weekdays at 09:00:00
OnCalendar=*-*-* 0/30:00:00Every 30 minutes, top and half-past
OnCalendar=*-*-* *:00/15:00Every 15 minutes
OnCalendar=*-*-1..7 04:00:00First 7 days of each month at 04:00:00

Migrating a crontab entry, step by step

Take a typical crontab line:

0 9 * * 1-5 /opt/scripts/backup.sh

The equivalent timer setup is two files. First the service that runs the command:

# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh

Then the timer that schedules it:

# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup weekdays at 9 AM

[Timer]
OnCalendar=Mon..Fri 09:00:00
Persistent=true

[Install]
WantedBy=timers.target

Enable it and confirm with:

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

Journald logs instead of blind /dev/null

With cron you redirect output to a log file yourself — or worse, to /dev/null. With a timer, every run is recorded by journald automatically:

journalctl -u backup.service    # job output
journalctl -u backup.timer      # timer activations
systemctl status backup.service # last run, exit code

The exit code and timestamps live in the unit status, so “did it run?” becomes one command instead of a grep over a growing log file.

Persistent, SecureMode and hardening

Persistent=true is the single biggest reason people move off cron: if the machine was powered off at 09:00, the timer fires immediately after boot instead of skipping silently. For sensitive jobs you can also harden the unit with sandbox directives such as ProtectSystem=strict, NoNewPrivileges=yes and PrivateTmp=yes — privileges cron never gives you. Run systemd-analyze security backup.service to see the exposure score.

When to stay with cron

Keep cron when the host does not run systemd, when you manage a fleet that still relies on crontab automation, or when you need per-user scheduling without touching unit files. For anything new on a modern distro, a timer is the safer default. The cron not running guide covers the classic crontab pitfalls you avoid entirely with timers.

Generate a valid expression

Prefer classic cron? Build your expression in plain English — the Cron Generator produces the correct 5-field string and previews the next runs, so “weekdays at 9 AM” never becomes a typo again.

Frequently asked questions

Can systemd timers replace cron entirely?

On modern distros running systemd, yes — a timer unit can replace virtually every crontab entry, including minute, hourly, daily, weekly and monthly schedules. The main reason to keep cron is legacy machines that still use a classic cron daemon or jobs that depend on cron-specific CRON_TZ handling.

How do I write OnCalendar schedules?

OnCalendar uses a more human format than cron: daily means every day at 00:00, *-*-* 09:00:00 means every day at 9:00, Mon..Fri 09:00:00 means weekdays at 9:00, and *-*-* 0/30:00:00 means every 30 minutes. Run systemd-analyze calendar 'expression' to verify and preview the next runs.

How do I migrate my crontab to systemd timers?

Create a .service unit with Type=oneshot that runs your command, a matching .timer unit with the OnCalendar line, then run systemctl enable --now your-timer.timer. Place both files in /etc/systemd/system and reload with systemctl daemon-reload.

Why did my timer run again right after a reboot?

Because Persistent=true was set on the timer. That flag makes systemd run a missed schedule immediately after the machine comes back up, instead of silently skipping it — the exact behavior you want for backups, but not for jobs that must never run twice.

How do I see timer and service logs?

Use journalctl -u my-timer.timer to inspect timer activations and journalctl -u my-service.service to see the job output. systemctl list-timers shows every active timer, its schedule and the last and next run times.

What does SecureMode or a strict sandbox give me in systemd timers?

Each timer runs in an isolated unit where you can harden the job: NoNewPrivileges, ProtectSystem, ReadOnlyPaths, PrivateTmp and similar directives run the job in a restricted sandbox. Cron gives you none of that out of the box.

Related guides