Systemd as a Cron replacement

Menu

Despite its very simple and effective syntax, installing new tasks in cron can be tricky. In fact:

  1. Cron tasks run in an environment different from that available in an interactive shell and a script running ok in a terminal might fail when installed as a cron job.
  2. As a consequence of the first point, the simplest way to test a cron job is installing it in cron and see what happens1. However, the maximum speed at which we can test changes to a cron task is once per minute, that is, the maximum frequency at which cron can schedule a task.
  3. Least but not last, cron uses a single log for all its tasks and analyzing the output produced by a failing cron job can be difficult to reconstruct.

There are alternatives to cron of which the simplest (in terms of installations, at least) is using systemd (see, e.g., Systemd as a cron replacement). So after many, many years with cron, I recently decided to try and switch my cron tasks to systemd.

The migration was mostly painless. I had to overcome a bit of friction to adapt to the new syntax fo calendar expressions, but it did not take take much time and I am now very happy with the new setup.

There is quite some documentation on the steps to perform the switch, with the Arch Wiki providing, as in many other cases, precise and simple guidelines. See, for instance, systemd/Timers, Systemd/User, and Systemd.

Here I recap the main steps and expand on the points which caused my the most difficulties.

Basic information about systemctl --user

  1. Systemd services can run in the user space, if the --user argument is specified. Each service is specified with a file stored in .config/systemd/user; there is no “installation” of the file: you just write it there.
  2. A systemd timer specifies how often a specific service has to be run. It is specifide with a file in .config/systemd/user, which references a service in the same directory.
  3. Timers have to be enabled and started, while the services referenced by a timer do not.

Creating a Timer

  1. Create a service file myunit.service in .config/systemd/user, which specifies the script(s) to run. For instance:
[Unit]
Description=Config Backup Service

[Service]
Type=oneshot
ExecStart=/home/adolfo/Sources/bash/pacman-list.bash
ExecStart=/home/adolfo/Sources/bash/systemd-backup.bash
  1. Create a timer file myunit.timer in .config/systemd/user, which specifies how often the service has to run. For instance, we decide to run the service at 9:10am:
[Unit]
Description=Backup important files for system configuration

[Timer]
OnCalendar=9:10
Persistent=true

[Install]
WantedBy=timers.target
  1. Enable and start the timer with:
systemctl enable --user myunit.timer
systemctl start --user myunit.timer

You can omit the --user option if you want to enable the service at the system level, that is, as root.

Frequency specification

The OnCalendar section specifies the frequency at which a task has to be run. The syntax is rather articulated. Some simple examples include:

  • HH:MM:SS: Run every day at HH:MM:SS
  • HH/FREQ:MIN: Run at HH:MIN and then at HH+(FREQ*N):MIN. For instance 8/2:20 runs every two hours starting at 8:20.
  • HH1..HH2/FREQ:MIN: Run between HH1 and HH2, starting at HH1:MIN and repeating every FREQ hours.
  • daily run every day at midnight

See the manual entries for systemd.timer and systemd.timers for more detailed information about the syntax supported.

Useful Commands

Check a calendar expression

systemd-analyze --iterations 2 calendar *-*-*

 Original form: *-2-29
Normalized form: *-02-29 00:00:00
    Next elapse: Thu 2024-02-29 00:00:00 CET
       (in UTC): Wed 2024-02-28 23:00:00 UTC
       From now: 3 years 5 months left
       Iter. #2: Tue 2028-02-29 00:00:00 CET
       (in UTC): Mon 2028-02-28 23:00:00 UTC
       From now: 7 years 5 months left

Run a service (e.g., for testing purposes)

systemctl --user start myunit.service

Install a timer

systemctl --user enable myunit.timer
systemctl --user start myunit.timer

List the timers which will run next

systemctl --user list-timers 

NEXT                         LEFT       LAST                         PASSED       UNIT                    ACTIVAT>
Sat 2020-09-05 17:30:00 CEST 11min left Sat 2020-09-05 16:30:42 CEST 48min ago    borg-local-backup.timer borg-lo>
Sat 2020-09-05 18:00:00 CEST 41min left Sat 2020-09-05 16:00:42 CEST 1h 18min ago borg-backup.timer       borg-ba>
Sun 2020-09-06 09:10:00 CEST 15h left   Sat 2020-09-05 11:42:44 CEST 5h 36min ago config-backup.timer     config->

3 timers listed.

View the output generated by a unit

journalctl -q --user-unit config-backup.timer

-- Logs begin at Mon 2020-07-20 09:03:57 CEST, end at Sat 2020-09-05 16:50:52 CEST. --
Sep 04 08:42:41 qonos systemd[1096]: Started Backup important files for system configuration.
Sep 04 11:26:32 qonos systemd[1096]: config-backup.timer: Succeeded.
Sep 04 11:26:32 qonos systemd[1096]: Stopped Backup important files for system configuration.
-- Reboot --
Sep 04 11:27:06 qonos systemd[1071]: Started Backup important files for system configuration.
-- Reboot --
Sep 05 12:54:46 qonos systemd[1134]: Started Backup important files for system configuration.
Sep 05 14:55:38 qonos systemd[1134]: config-backup.timer: Succeeded.
Sep 05 14:55:38 qonos systemd[1134]: Stopped Backup important files for system configuration.

Footnotes:

1

This is not completely true, as one could set an environment which simulates that of cron, for instance by running env as a cron task.