Convert a cron job to a systemd timer

Options

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/5 means "from 0, every 5". Cron's */5 becomes 00/5 for minutes and hours, and 01/5 for 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 :00 seconds.
  • Several OnCalendar= lines in one timer are combined with OR. The timer fires when any of them matches.

OnCalendar examples (cron to systemd)

CronOnCalendar=Meaning
* * * * **-*-* *:*:00Every minute
*/5 * * * **-*-* *:00/5:00Every 5 minutes
0 * * * **-*-* *:00:00 or hourlyEvery hour
0 */6 * * **-*-* 00/6:00:00Every 6 hours
0 3 * * **-*-* 03:00:00Daily at 03:00
0 3 * * 1Mon *-*-* 03:00:00Mondays at 03:00
*/15 9-17 * * 1-5Mon..Fri *-*-* 09..17:00/15:00Every 15 minutes, weekday working hours
0 0 * * 6,0Sat,Sun *-*-* 00:00:00Weekends at midnight
30 2 1 * **-*-01 02:30:001st of the month at 02:30
0 0 1 1 **-01-01 00:00:00 or yearlyOnce a year
(no cron equivalent)*-*~01 23:00:00Last 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 * 5 on every 13th or every Friday. One OnCalendar= 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. Add Environment= or EnvironmentFile= 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 with loginctl 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.