>_ josetilabs.com
← blog

03 Nov 2026 · linux, systemd, cron, automacao · 4 min

Migrating from cron to systemd timers

A systemd timer is a pair of files: a .timer that says when to fire and a .service that says what to run. OnCalendar takes calendar expressions like cron, Persistent=true recovers a missed run when the machine was off, and the log goes to the journal instead of becoming email. Migration is replacing the crontab line with these two files and running systemctl enable –now.

Why switch

cron does not know whether the machine was off at the scheduled time: the run is lost. cron’s log depends on manual redirection and MAILTO. A systemd timer records the last run on disk, recovers it with Persistent=true, and sends stdout and stderr to the journal, queryable with journalctl -u. The downside is verbosity: two units instead of one line.

The artifact: .service and .timer pair

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

[Service]
Type=oneshot
ExecStart=/opt/backup/backup.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=backup
ini
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup (timer)

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=0
Unit=backup.service

[Install]
WantedBy=timers.target

Type=oneshot is the natural partner of a timer: the service runs and exits, and systemd considers the activation done when the process leaves. OnCalendar=--* 03:00:00 fires every day at 3am; the syntax is year-month-day hour:minute:second, with * for any value. Persistent=true stores the last run on disk and, if the timer was inactive at the scheduled time (machine off), fires the service on boot. RandomizedDelaySec=0 disables the random delay; useful on a single machine, but in a fleet use a value to avoid hammering the target. Unit= names the service; if omitted, systemd looks for a service with the same name as the timer.

Install and start

text
sudo install -o root -g root -m 0644 backup.service backup.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

How to check

text
systemctl list-timers --all
systemd-analyze calendar "*-*-* 03:00:00"
systemctl status backup.timer
# force a run now:
sudo systemctl start backup.service
journalctl -u backup.service -e

list-timers shows the next run (NEXT) and the last (LAST). systemd-analyze calendar validates the expression and prints the next elapse with the countdown. Forcing the run with systemctl start backup.service runs the service once without waiting for the timer; that is how you test the script.

Terminal with two panels: systemd-analyze calendar showing next elapse at 07:03:00, and timer status with Trigger in 24s plus a manual run with status 0/SUCCESS
The calendar validates the expression and the manual run proves the service works without waiting for the timer.

When it goes wrong

Timer does not fire. Cause: invalid OnCalendar expression or inactive timer. Fix: validate with systemd-analyze calendar; check systemctl status backup.timer, which should be active (waiting) with a Trigger in the future.

Service does not run. Cause: wrong Unit= name or missing .service file. Fix: check the name in backup.timer and run systemctl daemon-reload.

Missed run not recovered. Cause: Persistent only works with OnCalendar; monotonic timers (OnBootSec) do not persist. Fix: use OnCalendar for wall-clock routines; for monotonic, accept the loss.

Timer fires only once. Cause: RemainAfterExit=yes on the oneshot service keeps it active, so the timer does not re-fire. Fix: remove RemainAfterExit on a service called by a timer.

Log does not show up. Cause: StandardOutput not set or journal not queried. Fix: StandardOutput=journal and StandardError=journal in the .service; read with journalctl -u backup.service.

Table: cron or timer

Feature cron systemd timer
Schedule syntax 5 fields OnCalendar (calendar)
Recover missed run no Persistent=true
Log file or email journalctl -u
Dependency on another service no After=, Requires=
Concurrency (no overlap) manual flock not native; use flock in the script
Complexity one line two files

FAQ

Can I migrate all jobs at once? Yes, but do them one at a time and check list-timers. Jobs that need a specific PATH work better with Environment= or EnvironmentFile.

Does the timer run while the machine is suspended? A calendar timer (OnCalendar) does not fire during suspension; on wake, if Persistent=true, it recovers if the time passed. A monotonic timer pauses with the machine.

How do I see the next run? systemctl list-timers shows NEXT and the remaining time. systemd-analyze calendar “--* 03:00:00” prints the next elapse.

How do I disable it? systemctl disable –now backup.timer. The service may keep running if already active; stop it with systemctl stop backup.service.

Does cron leave the system? No. The two coexist. Migrate what gains from the journal and recovery; leave cron for what is simple.

Sources

systemd.timer(5) and systemd.time(7), checked in November 2026. For services, systemd.service(5). For cron, cron done right. For logs, journalctl to find the error. For secrets in a file, env and secrets in a file.

Read next

// By e-mail

Posts published here also go out by e-mail. Delivery has not started yet: whoever leaves an address now joins the first batch.

I store only your e-mail, the date and the record of this consent. No cookies, no trackers, nothing shared with third parties. Details in the privacy policy.