All Articles

Replacing Cronjobs with Systemd Timers

Whenever I want to use a cronjob on Linux, I eventually run into some issues. I’ve spent whole nights debugging why a cronjob didn’t run correctly. By default there doesn’t seem to be any logging support so anything sent to STDOUT or STDERR just disappear. There’s also hardly any sort of indication that the job started except by checking logs.

Instead of writing cronjobs though, you can now use Systemd timers instead. Systemd nicely logs everything so it’s accessible via journalctl, and allows you to see the next scheduled execution time via systemctl list-timers.

How-to

In this examle I’ve created a quick and dirty python script that just prints to stderr and stdout. If you want to use it, save the following script as executable at /usr/local/bin/script.py.

#!/usr/bin/python3
import sys

print("Hello from script.py")
print("Writing to stdout")
print("Writing to stderr", file=sys.stderr)
print("Done!")

We first need to create the service systemd unit. As this won’t be a continuously running service, we use the oneshot service type. Save the following as /etc/systemd/system/script.service.

[Unit]
Description=A description of what your service does.

[Service]
Type=oneshot
ExecStart=/usr/local/bin/script.py

Note that ExecStart is not able to pass arguments to the specified program. At least not in a normal way. But for this example it works since it’s just a script with no arguments. If you need to pass arguments you can create a wrapper script.

Next we create the timer itself. It will be connected to the service by having the same filename, but with the different .timer extension. Save the following as /etc/systemd/system/script.timer.

[Unit]
Description=Run script every day at noon

[Timer]
OnCalendar=*-*-* 12:00:00
Persistent=true

[Install]
WantedBy=timers.target

OnCalendar allows you to set timers to execute on a “calendar” or “realtime” basis. It uses a format of DayOfWeek Year-Month-Day Hour:Minute:Second and like cron allows you to wildcard each component using *. Note that the syntax is slightly different from cron, e.g. to specify every 4 hours you would specify: *-*-* 0/4:00:00. You can test the format string via systemd-analyze calendar <format string>. For complete specification see: https://www.freedesktop.org/software/systemd/man/systemd.time.html

It’s also possible to set timers that execute on a “monotonic” basis, meaning they execute at a point relative to e.g. boot. Arch wiki has examples of both realtime and monotonic timers.

Next you need to enable the timer in systemd. You’ll need to run the following scripts.

systemctl daemon-reload # to reload available services
systemctl start script.timer # to start the timer, note that this won't start the timer on next boot.
systemctl enable script.timer # to automatically start the timer on boot (not the [Install section])

To test the timer, you can run the service using systemctl start script

Check logs using journalctl -u script.

root@mimikyu:/# journalctl -u script
-- Logs begin at Sun 2019-09-08 00:23:42 CEST, end at Sun 2020-02-02 17:21:21 CET. --
Feb 02 17:21:17 mimikyu systemd[1]: Starting A description of what your service does....
Feb 02 17:21:17 mimikyu script.py[14963]: Writing to stderr
Feb 02 17:21:17 mimikyu script.py[14963]: Hello from script.py
Feb 02 17:21:17 mimikyu script.py[14963]: Writing to stdout
Feb 02 17:21:17 mimikyu script.py[14963]: Done!
Feb 02 17:21:17 mimikyu systemd[1]: Started A description of what your service does..

See a table of active timers on your system using systemctl list-timers. Here’s what it looks like on my laptop.

root@mimikyu:/# systemctl list-timers
NEXT                         LEFT       LAST                         PASSED       UNIT                         ACTIVATES
Sun 2020-02-02 18:02:47 CET  38min left Sun 2020-02-02 17:03:00 CET  20min ago    anacron.timer                anacron.service
Mon 2020-02-03 00:00:00 CET  6h left    Mon 2020-01-27 06:33:47 CET  6 days ago   fstrim.timer                 fstrim.service
Mon 2020-02-03 01:37:32 CET  8h left    Sat 2020-02-01 12:59:50 CET  1 day 4h ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Mon 2020-02-03 04:47:10 CET  11h left   Sun 2020-02-02 11:28:40 CET  5h 55min ago apt-daily.timer              apt-daily.service
Mon 2020-02-03 06:33:12 CET  13h left   Sun 2020-02-02 11:28:40 CET  5h 55min ago apt-daily-upgrade.timer      apt-daily-upgrade.service
Mon 2020-02-03 06:35:38 CET  13h left   Sun 2020-02-02 16:58:23 CET  25min ago    motd-news.timer              motd-news.service
n/a                          n/a        Thu 2020-01-30 08:27:59 CET  3 days ago   ureadahead-stop.timer        ureadahead-stop.service

8 timers listed.
Pass --all to see loaded but inactive timers, too.

From what I understand, this can also be done on a user-level basis. So the the service will execute with that user’s profile. It works similarily but you put the systemd unit files in different directories. E.g. ~/.config/systemd/user/. The arch wiki has a detailed page about it.

References:

https://wiki.archlinux.org/index.php/Systemd/Timers https://www.freedesktop.org/software/systemd/man/systemd.service.html https://wiki.archlinux.org/index.php/Systemd/User