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.
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:00and similar readable expressions. - How do I avoid missing runs after downtime?
- Set
Persistent=trueon 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
| Dimension | cron | systemd timers |
|---|---|---|
| Schedule syntax | 5 fields (minute hour dom month dow) | OnCalendar, written in plain terms (daily, Mon..Fri 09:00:00) |
| Missed runs | Skipped silently if the daemon was down | Persistent=true replays missed runs at next boot |
| Logging | Syslog or wherever you redirect output | Built-in journald with journalctl -u <unit> |
| Dependencies | No ordering; fires when the time matches | Requires/After ordering against other units |
| Security hardening | None built in | Sandbox directives (ProtectSystem, NoNewPrivileges, PrivateTmp) |
| Environment | Near-empty PATH; you must export everything | Environment=/EnvironmentFile= supported natively |
| Management | crontab -e per user | systemctl 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 value | Meaning |
|---|---|
| OnCalendar=daily | Every day at 00:00:00 |
| OnCalendar=*-*-* 09:00:00 | Every day at 09:00:00 |
| OnCalendar=Mon..Fri 09:00:00 | Weekdays at 09:00:00 |
| OnCalendar=*-*-* 0/30:00:00 | Every 30 minutes, top and half-past |
| OnCalendar=*-*-* *:00/15:00 | Every 15 minutes |
| OnCalendar=*-*-1..7 04:00:00 | First 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.