Convert a cron job to a systemd timer
Cron meaning: Every 5 minutes.
OnCalendar: *-*-* *:00/5:00
/etc/systemd/system/backup.service
/etc/systemd/system/backup.timer
Install and verify
How to convert a cron job to a systemd timer
Paste the five time fields from your crontab line, the command and a short unit name. The converter writes two files. The .service unit says what to run, and the .timer unit says when to run it, with an OnCalendar= line equivalent to your cron expression. Copy both files into place, then run the install commands. Before you enable anything, always check the schedule with systemd-analyze calendar. It prints the normalized expression and the next run times, so a mistake shows up immediately.
systemd-analyze calendar --iterations=3 'Mon..Fri *-*-* 03:00:00'
Example output:
Original form: Mon..Fri *-*-* 03:00:00
Normalized form: Mon..Fri *-*-* 03:00:00
Next elapse: Mon 2026-10-19 03:00:00 CEST
(in UTC): Mon 2026-10-19 01:00:00 UTC
From now: 2 days left
Iteration #2: Tue 2026-10-20 03:00:00 CEST
...
Timers are standard systemd and work the same on Ubuntu, Debian, Fedora, Arch and openSUSE. Many distributions already ship their own maintenance jobs as timers (systemctl list-timers --all shows logrotate.timer, fstrim.timer and others).
OnCalendar format
An OnCalendar= value has the shape DayOfWeek Year-Month-Day Hour:Minute:Second. You can leave out the weekday, and * matches any value. A few rules differ from cron:
- Ranges use
..instead of-:Mon..Fri,09..17. - Steps are written
start/step:00/5means "from 0, every 5". Cron's*/5becomes00/5for minutes and hours, and01/5for days and months. - Weekdays use English abbreviations (
Mon,Tue...), and systemd orders them Monday to Sunday. - Seconds are included. A converted cron job always fires at
:00seconds. - Several
OnCalendar=lines in one timer are combined with OR. The timer fires when any of them matches.
OnCalendar examples (cron to systemd)
| Cron | OnCalendar= | Meaning |
|---|---|---|
* * * * * | *-*-* *:*:00 | Every minute |
*/5 * * * * | *-*-* *:00/5:00 | Every 5 minutes |
0 * * * * | *-*-* *:00:00 or hourly | Every hour |
0 */6 * * * | *-*-* 00/6:00:00 | Every 6 hours |
0 3 * * * | *-*-* 03:00:00 | Daily at 03:00 |
0 3 * * 1 | Mon *-*-* 03:00:00 | Mondays at 03:00 |
*/15 9-17 * * 1-5 | Mon..Fri *-*-* 09..17:00/15:00 | Every 15 minutes, weekday working hours |
0 0 * * 6,0 | Sat,Sun *-*-* 00:00:00 | Weekends at midnight |
30 2 1 * * | *-*-01 02:30:00 | 1st of the month at 02:30 |
0 0 1 1 * | *-01-01 00:00:00 or yearly | Once a year |
| (no cron equivalent) | *-*~01 23:00:00 | Last day of every month at 23:00 |
The shorthands minutely, hourly, daily, weekly (Monday 00:00, unlike cron's Sunday), monthly and yearly also work. The full grammar is in systemd.time(7).
What the generated units do
Type=oneshot tells systemd the job runs and exits, so the service shows as inactive between runs. That is normal. Persistent=true stores the last run time on disk. If the machine was off or asleep when the job was due, it runs once at the next boot, which is the main advantage over cron for laptops and desktops. RandomizedDelaySec= spreads the start time out, so many machines do not all hit a server at the same second. The [Install] section ties the timer to timers.target. You enable the timer, not the service.
Output goes to the journal instead of mail. Read it with journalctl -u NAME.service, see the journalctl cheat sheet, and manage the units with the commands in the systemctl cheat sheet. If a run fails, systemctl status NAME.service shows the exit code. The "Job for X.service failed" guide explains how to read it.
Limitations and differences from cron
- Day-of-month OR day-of-week. Cron runs
0 0 13 * 5on every 13th or every Friday. OneOnCalendar=line means "13th and Friday". The converter writes two OnCalendar lines to keep the cron meaning. Check that this is what you actually want. - The % character. In unit files,
%starts a specifier (%h,%u). It must be written%%. The converter also removes cron's\%escaping. - Shell syntax.
ExecStart=does not run a shell. Pipes, redirects,&&and$(...)need/bin/sh -c '...', and$must be written$$. Moving complex commands into a script is cleaner. - Environment. Variables set at the top of a crontab (
MAILTO,PATH) are not carried over. AddEnvironment=orEnvironmentFile=lines to the service if you need them. - Who runs it. A system unit runs as root unless you set
User=. A user timer (~/.config/systemd/user) runs only while you are logged in, unless lingering is enabled withloginctl enable-linger. - @reboot. There is no calendar value for it. The converter uses
OnBootSec=and adds network ordering to the service. - Overlapping runs. Unlike cron, systemd never starts a second copy of a service that is still running. Usually that is what you want.
Not sure you should switch at all? Read cron vs systemd timers. To build or decode the cron expression first, use the crontab generator. For unit-file basics, see the systemd basics guide.
Frequently asked questions
What is the OnCalendar value for every 5 minutes?
OnCalendar=*-*-* *:00/5:00, which you can shorten to *:0/5. Check it with systemd-analyze calendar '*:0/5'.
How do I test an OnCalendar expression?
Run systemd-analyze calendar --iterations=5 'EXPRESSION'. It prints the normalized form and the next run times without creating any unit.
What does Persistent=true do in a systemd timer?
It saves the time of the last run to disk. If the machine was off when a run was due, the job runs once as soon as the timer starts again after boot.
Do I enable the .service or the .timer?
Enable the timer: sudo systemctl enable --now NAME.timer. The timer starts the service. Start the service by hand only to test it.
Why does my systemd timer not run when I am logged out?
User timers in ~/.config/systemd/user stop when your last session ends. Run loginctl enable-linger $USER, or install the units as system units with User=.
Spotted a mistake or a command that no longer works? Send a correction — pages are re-checked when reports come in.