Quick comparison
| Cron | systemd timers | |
|---|---|---|
| Files per job | One line in a crontab | Two files: .service + .timer |
| Schedule syntax | 5-field cron | OnCalendar=Mon..Fri *-*-* 09:00:00 |
| Logging | Stderr to mailbox (often lost) | journald, queryable with journalctl |
| Missed runs | Lost (unless anacron is installed) | Run at next boot with Persistent=true |
| Resource limits | None built-in | Full cgroup limits (memory, CPU, I/O) |
| Dependencies | None | Full service dependency chains (After=, Requires=) |
| Timezone | System or CRON_TZ | Per-timer timezone in OnCalendar |
| Available since | Forever | systemd 209 (2014); systemd-everywhere by ~2017 |
systemd timer syntax
A systemd timer consists of two units: a .service defining what to run and a .timer defining when.
The service file
/etc/systemd/system/nightly-backup.service:
[Unit] Description=Nightly backup [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh User=backup WorkingDirectory=/var/lib/backup
The timer file
/etc/systemd/system/nightly-backup.timer:
[Unit] Description=Nightly backup timer [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target
Activating the timer
sudo systemctl daemon-reload sudo systemctl enable --now nightly-backup.timer systemctl list-timers # Verify it's loaded
OnCalendar syntax
More expressive than cron, with named days and clearer time format:
| Expression | Meaning |
|---|---|
OnCalendar=*-*-* 09:00:00 | Every day at 9 AM |
OnCalendar=Mon..Fri *-*-* 09:00:00 | Weekdays at 9 AM |
OnCalendar=*-*-01 00:00:00 | First of every month at midnight |
OnCalendar=Mon *-*-* 09:00:00 | Every Monday at 9 AM |
OnCalendar=Mon *-*-* 09:00:00 America/New_York | Same, in Eastern time |
OnCalendar=*:0/15 | Every 15 minutes |
OnUnitActiveSec=1h | One hour after the previous run finished |
Where systemd timers win
1. Missed-run handling
If the system was off when the timer should have fired, Persistent=true ensures it runs at next boot. Cron silently skips missed runs unless you set up anacron separately.
2. Logging
Every run's output, exit code, and duration are captured by journald:
journalctl -u nightly-backup.service # All runs journalctl -u nightly-backup.service -f # Tail live systemctl status nightly-backup.timer # When did it last run? Next?
3. Resource limits
You can constrain memory, CPU, and I/O via cgroups in the service file:
[Service] ExecStart=/usr/local/bin/heavy-job.sh MemoryMax=2G CPUQuota=50% IOWeight=10
This is impossible with classic cron without external tooling.
4. Dependency chains
A timer can require other services to be active first:
[Unit] Description=Generate report after database backup Requires=postgres-backup.service After=postgres-backup.service
5. Timezone correctness
You can specify the timezone inline in OnCalendar, avoiding the trap of forgetting to set CRON_TZ.
6. Easier debugging
To run a job once on demand: systemctl start nightly-backup.service. You get the same logging, exit code, and resource accounting as a scheduled run. With cron, you'd have to manually invoke the script.
Where cron is still better
1. Verbose for simple jobs
One crontab line vs two systemd files. For a "run this script daily" job, cron is just faster to set up.
2. Universally understood
Every Linux/macOS sysadmin knows cron syntax. Many know systemd timers in theory but write them rarely.
3. Portable
Cron works on macOS, BSD, Alpine, busybox, container images without systemd. systemd timers are Linux-systemd only.
4. Per-user scheduling
Cron has crontab -e as an unprivileged operation. systemd has user-mode timers (--user) but they require additional setup.
Migrating from cron to systemd timers
Take an existing crontab entry like:
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
It becomes a service file and a timer file:
/etc/systemd/system/backup.service:
[Unit] Description=Daily backup [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh StandardOutput=append:/var/log/backup.log StandardError=append:/var/log/backup.log
/etc/systemd/system/backup.timer:
[Unit] Description=Daily backup at 2 AM [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target
Enable and verify:
sudo systemctl daemon-reload sudo systemctl enable --now backup.timer systemctl list-timers backup.timer
Recommendation
For modern Linux production servers: systemd timers. The logging, missed-run handling, and resource limits more than make up for the extra verbosity.
For containers, simple cron servers, and macOS dev environments: cron remains fine. There's no urgency to migrate working systems.