cronuru
Guide

systemd Timers vs Cron — A Complete Guide

systemd timers are the modern Linux alternative to crontab. They use a different syntax (OnCalendar, not cron expressions), pair with service units instead of running shell commands directly, integrate with the systemd journal for logs, and survive reboots without missing scheduled runs. This guide covers the syntax, the migration story, and when crontab is still the right choice.

Updated

Why systemd timers

Cron has been the standard Linux scheduler for forty years. systemd timers, introduced around 2010, are the modern alternative on systems running systemd (which is almost every mainstream Linux distribution: Ubuntu, Debian, Fedora, RHEL, Arch, etc.).

What systemd timers do better:

  • Catch-up after downtime. A timer with Persistent=true runs missed schedules when the system comes back online. cron has no equivalent — missed runs are missed forever.
  • Better logging. Output goes through the systemd journal, queryable with journalctl -u my-job.service. cron writes to syslog, which is harder to filter and often discarded.
  • Resource control. The service unit can declare CPU/memory limits, IO priority, network namespace, etc. cron has none of this.
  • Easier debugging. systemctl status my-job.timer and systemctl status my-job.service show the current state, last fire time, last exit status, and recent log lines.
  • Dependencies. A timer can be ordered after other units (After=network-online.target), preventing the classic cron bug of jobs that run before the network is up.
  • Sub-minute precision. systemd timers can fire every 30 seconds, every 5 seconds, etc. cron’s minimum is 1 minute.

What cron does better:

  • Lower ceremony. One line in a crontab vs. two unit files for a timer.
  • Per-user crontabs. crontab -e “just works” for any logged-in user. systemd’s user mode (systemctl --user) works but is less ubiquitous.
  • Portability across non-systemd Linux. Alpine, Slackware, some embedded systems don’t ship systemd.
  • Familiarity. Every Linux engineer knows cron.

For a single long-running production job on a modern distro, systemd timers are the better choice. For a one-line backup script you’ll forget about, crontab is fine.

Timer + service anatomy

A systemd timer is two files: a .timer unit (when to fire) and a matching .service unit (what to run).

/etc/systemd/system/nightly-backup.service:

[Unit]
Description=Nightly database backup

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

/etc/systemd/system/nightly-backup.timer:

[Unit]
Description=Run nightly-backup.service daily

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

[Install]
WantedBy=timers.target

The convention is to name the timer and service identically (nightly-backup.timer + nightly-backup.service) so systemd automatically pairs them. To use a different service name, set Unit=other-name.service in the [Timer] block.

Enable and start the timer:

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

enable makes it persist across reboots; --now starts it immediately.

OnCalendar syntax

OnCalendar= accepts systemd’s calendar specification — not cron syntax. The format is:

DayOfWeek Year-Month-Day Hour:Minute:Second

Each field can be * (any), a specific value, a comma list, a range, or a step. Familiar territory if you know cron, but the syntax is different.

Cronsystemd OnCalendarMeaning
0 0 * * **-*-* 00:00:00 or dailyDaily at midnight
0 * * * **-*-* *:00:00 or hourlyEvery hour
*/5 * * * **-*-* *:00/5:00Every 5 minutes
0 9 * * 1-5Mon..Fri 09:00:00Weekdays at 9 AM
0 0 1 * **-*-01 00:00:00 or monthly1st of every month
0 0 * * 0Sun 00:00:00 or weeklySundays at midnight
0 0 1 1 **-01-01 00:00:00 or yearlyJanuary 1st

Named shortcuts: minutely, hourly, daily, weekly, monthly, quarterly, semiannually, yearly.

Test a calendar specification with systemd-analyze:

systemd-analyze calendar "Mon..Fri 09:00:00"
# Original form: Mon..Fri 09:00:00
# Normalized form: Mon..Fri *-*-* 09:00:00
#  Next elapse: Mon 2026-05-25 09:00:00 PDT
#  (in UTC): Mon 2026-05-25 16:00:00 UTC
#  From now: 13h 7min left

This is the equivalent of pasting a cron expression into the parser — it tells you exactly when the timer will fire next, in your local timezone.

There are also relative timer triggers:

  • OnBootSec=10min — fire 10 minutes after boot
  • OnUnitActiveSec=1h — fire 1 hour after the unit was last active (good for “every hour from now” semantics that cron can’t express)
  • OnStartupSec= — fire after systemd itself started

You can combine multiple OnCalendar= and OnUnitActiveSec= entries in the same [Timer] block.

Persistent timers

The most operationally important difference from cron:

[Timer]
OnCalendar=daily
Persistent=true

Persistent=true tells systemd to remember when the timer last fired (in /var/lib/systemd/timers/). If the system is down at a scheduled time, the timer fires immediately when the system comes back up.

cron has no equivalent. A daily-at-midnight cron job on a server that was offline at midnight runs the next day. With Persistent=true, it runs as soon as the server is back up.

For backups, log rotation, and other “must happen eventually” jobs, Persistent=true is essentially mandatory. Without it, missed runs are silently dropped.

For high-frequency timers (every 5 minutes), be careful — Persistent=true plus a multi-hour outage means a burst of catch-up runs. Pair with RandomizedDelaySec= to spread them out:

[Timer]
OnCalendar=*-*-* *:00/5:00
Persistent=true
RandomizedDelaySec=30s

Managing timers

List all active timers and their schedules:

systemctl list-timers

Output shows next fire time, last fire time, and the timer/service names. Sort by next fire time by default — useful “what’s about to run” view.

Add --all to include disabled timers, or --user for per-user timers.

Per-timer commands:

# Status
systemctl status nightly-backup.timer
systemctl status nightly-backup.service

# Disable temporarily (won't fire until re-enabled)
sudo systemctl stop nightly-backup.timer
sudo systemctl disable nightly-backup.timer

# Run the service manually right now (bypassing the timer)
sudo systemctl start nightly-backup.service

# Show the calendar spec next fires
systemd-analyze calendar "Mon..Fri 09:00:00" --iterations=5

Migrating from crontab

The mechanical translation from a crontab line to a timer + service pair:

Original crontab:

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

/etc/systemd/system/backup.service:

[Unit]
Description=Nightly backup

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

/etc/systemd/system/backup.timer:

[Unit]
Description=Run backup daily at 2 AM

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

[Install]
WantedBy=timers.target

Then:

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
crontab -e   # delete the old line

For a crontab with many entries, you’ll generate many .timer/.service pairs. Some teams use a single shared .service file with multiple .timer files pointing at it via Unit=shared.service — useful when several schedules call the same script with different arguments (passed via Environment= in the timer).

If you maintain a lot of crontabs, the migration is mostly worth it for Persistent=true and the unified logging. If you have three lines, leave it in crontab.

Logs and debugging

journalctl is the entry point. Common queries:

# All output from the service
journalctl -u nightly-backup.service

# Just the last invocation
journalctl -u nightly-backup.service -n 50

# Follow in real time (like tail -f)
journalctl -u nightly-backup.service -f

# Only since the last boot
journalctl -u nightly-backup.service -b

# Errors only
journalctl -u nightly-backup.service -p err

The journal captures both stdout and stderr from the service automatically — no need for >> /var/log/myjob.log 2>&1 redirection like in crontab.

For “is the timer firing at all” questions, systemctl list-timers shows the last fire time. If it’s stale or n/a, the timer isn’t active. Run systemctl status backup.timer to see why (enabled/disabled/failed).

Common diagnostic patterns:

  • Timer is enabled but service never runs — usually Unit= mismatch or the calendar spec doesn’t fire when you expected. Use systemd-analyze calendar "..." to verify.
  • Service runs but fails immediatelysystemctl status shows the exit code. journalctl -u service.service -n 50 shows recent output including the failure reason.
  • Service runs but the wrong user/env — set User=, Group=, Environment=, and WorkingDirectory= explicitly in the service unit. Don’t rely on shell login defaults.

When to keep cron

Keep crontab for:

  • One-line scripts where the ceremony of two unit files is overkill
  • Per-user scheduled taskscrontab -e is simpler than systemctl --user
  • Non-systemd distributions — Alpine, Slackware, some embedded systems
  • macOS — uses launchd, not systemd. crontab also works on macOS but is gradually being deprecated in favor of LaunchAgents
  • Maintenance scripts on systems you don’t fully control — root crontab is universal; systemd unit installation varies by distro and configuration

For everything else on a modern Linux server, systemd timers give you better catch-up semantics, integrated logging, and easier debugging. If you’re starting from scratch, prefer timers. If you have a working crontab, only migrate if you’re hitting specific limitations.

Either way, the cron parser works for the Unix expressions you have in crontab — paste them in to verify before any migration.

Frequently asked questions

What's the main advantage of systemd timers over cron?
Three things: (1) timers and the services they run are separate unit files, making it easier to test the service independently and to share work between timed and non-timed invocations. (2) `Persistent=true` lets timers catch up after the system was down. (3) Logs go through the systemd journal alongside everything else, so `journalctl -u my-job.service` shows everything.
Are systemd timers better than cron?
For long-running production work on modern Linux, yes — better catch-up behavior, integrated logging, easier debugging, more flexible scheduling. For one-off scripts and root-owned ad-hoc tasks, crontab is still simpler. Both work, both are reliable; pick based on the operational fit.
What is OnCalendar syntax?
systemd's own scheduling syntax, different from cron. Format: `DayOfWeek Year-Month-Day Hour:Minute:Second`. Examples: `Mon..Fri 09:00:00` (weekdays at 9 AM), `*-*-01 00:00:00` (1st of every month at midnight), `hourly`, `daily`, `weekly` (named shortcuts).
Can systemd timers run more often than every minute?
Yes. systemd timers go down to microsecond precision via `OnCalendar=*-*-* *:*:00/30` (every 30 seconds) or `OnUnitActiveSec=30s` (every 30 seconds from the last run). cron can't do sub-minute scheduling natively.
How do I list all timers on a system?
`systemctl list-timers` shows all timers, their next fire time, and when they last fired. Add `--all` to include inactive timers.