Jonathan Bowman

Schedule jobs with systemd timers, a cron alternative

· Updated

Categories: Linux

Schedule jobs with systemd timers, a cron alternative

For any sysadmin, devops engineer, or general Linux enthusiast, automating the annoying/boring/difficult stuff is crucial. And task scheduling plays a key role in automation.

For scheduling jobs, the old standby is cron. A central file (the crontab) contains the list of jobs, execution commands, and timings. Provided you can master the schedule expressions, cron is a robust and elegant solution.

For Linux sysadmins there is an alternative that provides tighter integration with systemd, intuitively named systemd timers.

While unlikely, you may use a Linux distribution (or BSD or other Unix-like system) that does not have systemd. If you use *BSD, Alpine Linux, Gentoo, Knoppix, Void, Tiny Core, Devuan, Artix Linux, and others using a different init system other than systemd, then this article may be just a curiosity. Read on, or simply enjoy the cron you have.

Choosing systemd timers instead of cron

If cron works, why use systemd timers? I do not believe this is a question about superiority. Both work well, and have pros and cons.

I turn to systemd timers in the following cases:

On the other hand, cron may win out if you want straightforward email notifications, and you and your team are highly familiar with the tool already.

The ArchWiki also lists some excellent benefits and caveats of using systemd timers over cron.

The service

For a systemd timer, there are two files that need to be created:

  1. The service that will be started
  2. The timer that schedules the service

I find the nomenclature of service a little confusing here. But it should be pointed out that a systemd service does not need to be a long running one. In this case, a “oneshot” service will do nicely. Even though it exits immediately, it is still called a service.

A simple example, to be installed as /etc/systemd/system/motd-weather.service

[Unit]
Description=Update message of the day with current weather

[Service]
ExecStart=/usr/bin/curl -o /etc/motd http://wttr.in/?1Fq
Type=oneshot

A few notes:

Before we install the timer, it is possible to test the service.

sudo systemctl start motd-weather.service

Did the above change /etc/motd?

Excellent.

The timer

Timers with systemd are a two-file system. We did one part, the service, and now we make a matching timer for scheduling the service.

A timer file associated with the example service above:

[Unit]
Description=Download weather to motd nightly

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h

[Install]
WantedBy=timers.target

If the service above is in /etc/systemd/system/motd-weather.service, then this file should be /etc/systemd/system/motd-weather.timer. Notice that the only difference is the extension: .service vs. .timer while the stem motd-weather is kept the same for auto-discovery.

A few notes:

Enable and start the timer

Assuming we have tested the service and it works as it should, we are ready to start the timer. To do so:

sudo systemctl enable --now motd-weather.timer

Notice that we enable and start the timer, and the timer then calls the service when scheduled. We do not start the service directly.

If it installed correctly, you should see it and some scheduling information when listing the systemd timers:

systemctl list-timers

If the list is long, you can filter with wildcards:

systemctl list-timers motd*

Calendar expressions

One component of systemd timers worth some ongoing study (or at least a browser bookmark) is what OnCalendar is set to: the calendar expression.

A few examples that may help introduce the syntax:

As seen above, * is used to mean “every.” Sometimes, to remind myself of the format, I run systemd-analyze timestamp now to see the normalized format for this current second, then start substituting * in the right places, changing the date, time, and timezone as appropriate. As soon as you start substituting with *, systemd-analyze timestamp no longer works; instead, use systemd-analyze calendar (see below).

You may also use these shorthand expressions: minutely, hourly, daily, monthly, weekly, yearly, quarterly, or semiannually.

To see a list of possible timezones, try timedatectl list-timezones

systemd-analyze calendar is your friend

You can test any of the above using systemd-analyze calendar. For instance, I want my service to run every Monday, Wednesday, and Friday at 11pm UTC, but not in December. Did I get it right? Let’s check the validity of the syntax.

systemd-analyze calendar "Mon,Wed,Fri *-1..11-* 23:00 UTC"

Eureka! It checked out OK. I can be happy, but I can also learn from the normalized form, and tweak the above to be Mon,Wed,Fri *-01..11-* 23:00:00 UTC instead.

One very useful sanity check: systemd-analyze calendar can show several of the next iterations, just to reassure you and help you think through when things will happen. For instance, I want the service to launch the first and third Wednesday of every month. That takes a bit more complexity than some other examples, so I want to be sure I have it right. The following will show me a year’s worth (24 occurrences) of such events.

systemd-analyze calendar --iterations=24 "Wed *-*-1..07,15..21 02:00"

After painstakingly looking at all 24 while perusing a desk calendar, everything checks out OK. Oh, but wait, I wanted to see the calendar year, not just a year from now. For that, we can use the --base-time option, and pick January 1 of the desired year. How about 2026:

systemd-analyze calendar --base-time="2026-01-01" --iterations=24 "Wed *-*-1..07,15..21 02:00"

Once your calendar expression checks out OK, set OnCalendar= to your chosen expression, in the timer file ending in .timer

Countdown timers

A systemd timer does not have to have a single or repeated calendar event. In other words, there are options other than OnCalendar=.

These include:

Again, you may wish to use systemd time span abbreviations, so you can give the time in hours or days, if seconds seems to lack readability.

User service manager

A systemd timer and service do not need to be installed in /etc/systemd/system/ and therefore run at the system level. Instead, they can be installed per user, and run within the user service manager, usually launched upon login. The ArchWiki has a great article about systemd user units, and the official systemd unit guide is a good reference.

The timer and service above will work fine when installed in ~/.config/systemd/user/, but of course the service would be unable to write to /etc/motd. Something like ExecStart=/usr/bin/curl -o %E/motd http://wttr.in/?1Fq would work better, but would also require something like cat ~/.config/motd at the end of your .bashrc or other shell script executed at login. Like that %E to refer to $XDG_CONFIG_HOME (usually ~/.config)? See the list of variables available in systemd unit files.

The one big caveat with user services: they don’t necessarily run at boot. Instead, they run at login. There is a nice workaround, though. If you want your user services and timers to run at boot, not just login, you can make a particular user “linger”. Then things work even when the user has not explicitly logged in. To do this:

sudo loginctl enable-linger my_username

And substitute my_username with the username you want to linger after reboot.

The docs

You may enjoy reading systemd’s documentation regarding timers and units:

Please feel free to post ideas, advice, and questions in the comments!