Systemd Timers
Posted on 2025-02-01 in Programmation
After using anacron for years to run a backup script regularly, I decided to have a look at systemd timers. Overall, anacron worked fine: I could run tasks as my user and it would start tasks if they missed a run. But, I was still frustrated with how it worked: I had to configure it to be able to run cron tasks with a non root user and never remembered where the config file was. I also wanted better notifications in case of failure: CRON programs send logs by email by default which is of no use to me on my local machine. And I am in the process of using systemd more on my servers. So it only makes sense to use it more locally too and to rely on its features to replace CRON jobs.
After digging a bit, the feature that convinced me to use systemd timers is the systemctl --user list-timers command (omit the --user option to list system timers): it lists all the timers with their next and last execution dates as well as how much time is remaining before the next run. I can see everything in a human readable way with one command! Of course, everything ends logged in journald! As with any systemd service, you can also specify dependencies of the service to run. I can also use Weekly and Monthly keywords to more easily define when to run a task without bothering with CRON syntax! I couldn’t ask for more.
Note
With Weekly and Monthly, scripts will run of the 1st day of the week or of the month at midnight which is fine for the small scripts I have, but may run lots of big scripts at the same time on your setup.
The only pain point I see is when writing a new timer. Timers are divided in two parts: the timer itself which define when something must run and an associated service file to run something. It’s a bit tiresome to write, but since I don’t do it often, it’s not a very big deal. On the bright side, you can run the service without the timer and it will behave the same.
Now that the introduction is over, let’s look at an example.
Note
I’ll focus on user run services since that’s what I need, but if you save the files under /etc/systemd/system and omit --user from the commands, you can create custom and system wide timers and services.
All the config files must be under ~/.config/systemd/user/. Here is a sample .timer file to help you get started (for instance backup-stuff.timer):
[Unit] Description="Weekly backup" [Timer] # Supports keywords like weekly and monthly and CRON syntax. OnCalendar=weekly # Run now if the scheduled run was missed. Persistent=true # Which service to run with this timer. Unit=backup-stuff.service [Install] WantedBy=timers.target
And the accompanying .service file (backup-stuff.service):
[Unit] Description="Sync server backup to the local machine" # Specify dependencies as usual in systemd units. After=network.target # %n is the name of the current unit. # This service will be called in case of failure with the name of the current unit # so it can mention it in its error message. OnFailure=notify-errors@%n.service [Service] # Should be a oneshot service: we start with the timer, the script runs and then stops. Type=oneshot # %h will be replaced by the home directory of the user. ExecStart=%h/bin/backup-servers.sh [Install] WantedBy=default.target
If you want to run it outside the timer, you can do systemctl --user start backup-stuff.service. Even if you do that, the service won’t skip its next timer run. That’s very useful for debugging. And just like a normal run, the logs will end up in journald.
And now, the service to notify failures, saved in .config/systemd/user/notify-errors@.service:
[Unit] Description="Notify errors to the user" [Service] Type=oneshot # %i will be replaced by the parameter (ie what comes after @ when the service was called). ExecStart=%h/bin/systemd-notify-errors.sh %i
And the accompanying script:
service=$1 # Get the last execution timestamp to filter the logs for this execution. last_execution_timestamp=$(systemctl --user show --property ExecMainStartTimestamp --value $service) # Extract the logs of this unit. logs=$(journalctl --user --unit $service --since "$last_execution_timestamp") # Display the error to the user. # Notification is displayed during 5 minutes (in ms). # This requires the libnotify-tools package. notify-send --urgency normal --expire-time 300000 "$service failed" "$logs"
Don’t forget to refresh the systemd daemon with systemctl --user daemon-reload or you won’t be able to see your new services and timers.
All we have left to do is to enable the timer with systemctl --user enable backup-stuff.timer And we are done!
Note
User timers will only be executed for users that logs in by default. To always execute them when the machine is up even if the user don’t log in, enable lingering with sudo loginctl enable-linger USER
Resources:
- systemd/Timers on the Archlinux wiki.
- Working with ``systemd` Timers <https://documentation.suse.com/smart/systems-management/html/systemd-working-with-timers/index.html>`__ on the openSUSE documentation.