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=trueruns 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.timerandsystemctl status my-job.serviceshow 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.
| Cron | systemd OnCalendar | Meaning |
|---|---|---|
0 0 * * * | *-*-* 00:00:00 or daily | Daily at midnight |
0 * * * * | *-*-* *:00:00 or hourly | Every hour |
*/5 * * * * | *-*-* *:00/5:00 | Every 5 minutes |
0 9 * * 1-5 | Mon..Fri 09:00:00 | Weekdays at 9 AM |
0 0 1 * * | *-*-01 00:00:00 or monthly | 1st of every month |
0 0 * * 0 | Sun 00:00:00 or weekly | Sundays at midnight |
0 0 1 1 * | *-01-01 00:00:00 or yearly | January 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 bootOnUnitActiveSec=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. Usesystemd-analyze calendar "..."to verify. - Service runs but fails immediately —
systemctl statusshows the exit code.journalctl -u service.service -n 50shows recent output including the failure reason. - Service runs but the wrong user/env — set
User=,Group=,Environment=, andWorkingDirectory=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 tasks —
crontab -eis simpler thansystemctl --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?
Are systemd timers better than cron?
What is OnCalendar syntax?
Can systemd timers run more often than every minute?
How do I list all timers on a system?
Related
Every 5 Minutes
Both crontab and systemd timers can express this.
PatternDaily at Midnight
The classic nightly job, in both syntaxes.
ToolCron Parser
Verify the cron expressions in your existing crontab before migrating.
GuideCron Dialect Comparison
How the 6 cron dialects compare — Unix is what crontab uses.