Scheduling tasks with systemd timers on Linux

Systemd is a service and system manager comprised of a collection of tools to perform different system tasks. One such tool is systemd timers, whose primary purpose is to schedule and run tasks during startup or repeatedly after a system boot.

Systemd timers are an alternative to the scheduler cron or anacron. For sysadmins, scheduling tasks play a crucial role in automating your system’s boring or difficult tasks. This article is an introductory guide to system timers, their structure, and configurations with real-world examples.

Why systemd timer

Like cron, systemd timers can also schedule tasks to be run at a granularity ranging from minutes to months or more. However, timers can also do certain things that cron cannot. For example, a timer can trigger a script to run at a specific period after an event such as boot, startup, completion of a previous task, or completing a service unit. Other benefits of timers over cron include:

  • systemd is already available, and you do not need to install any packages, unlike cron.
  • It makes it easy to enable, disable, or run individual tasks.
  • Logging is integrated and accessible with journalctl.
  • It provides the ability to run any missed or failed tasks at the next boot.
  • You can easily configure randomized delays.
  • You can test a task by itself without waiting for the schedule, which simplifies debugging.
  • Jobs can be attached to cgroups.
  • It offers robust Time Zone handling.
  • You can configure each job to run in a specific environment.

Caveats

  • Creating a task can be more verbose than cron. You need to create at least two files before you run systemctl commands.
  • There is no built-in email equivalent to cron’s MAILTO for sending emails on job failure.

Creating a task

Scheduling a task via a systemd requires at least two unit files: service unit and timer unit. A service unit file defines the actual command to be executed, while a timer unit file defines the schedule.

Demo

This demo is an example of a user-scheduled python script [birthday_countdown_app.py]that writes a message and a countdown of days to or after your birthday in the current year.

Create a python script

Create a virtual environment in home username/:

$ virtualenv venv

Start using local python:

$ source venv/bin/activate

Create a python script [birthday_countdown_app.py]:

$ sudo nano birthday_countdown_app.py
import datetime, time
#a birthday countdown app 

def get_birthday_from_user():
    year = 1996 #update your birth year
    month =10 #update your birth month
    day =3 #update your birth day
    birthday = datetime.date(year, month, day)
    return birthday 

def compute_days_between_dates(original_date, target_date):
    this_year = datetime.date(target_date.year, original_date.month, original_date.day)
    dt = this_year - target_date
    return dt.days 

def print_to_file(days):
    path_to_file = "/home/tuts/bc.txt" #address of output text file
    while True:
        with open(path_to_file, "a") as f:
            if days <0:
                f.write("nYou had your birthday {} days ago this year".format(-days))
                f.close()
            elif days >0:
                f.write("nIt is your birthday in {} days".format(days))
                f.close()
            else:
                f.write("nHappy Birthday!!!!")
                f.close()
         time.sleep(450) 

def main():
    bday = get_birthday_from_user()
    now = datetime.date.today()
    number_of_days = compute_days_between_dates(bday, now)
    print_to_file(number_of_days) 

main()

The above python script [birthday_countdown_app.py] will write a message and countdown of days to or after your birthday to a text file [bc.txt] in your home user directory.

Create a service unit file

The next step is to create the .service unit file that will do the actual work and call the python script above. Finally, we will configure the service as a user service by creating the service unit file in /etc/systemd/user/.

$ sudo nano /etc/systemd/user/birthday_countdown.service

[Unit]
Description=Update message with a current countdown to your birthday
[Service] 

Type=simple
ExecStart=/home/tuts/venv/bin/python /home/tuts/birthday_countdown_app.py
Type=oneshot

Check the status of the service:

$ systemctl --user status birthday_countdown.service
● birthday_countdown.service
Loaded: loaded (/etc/xdg/systemd/user/birthday_countdown.service; static)
Active: inactive (dead)

service unit status
Check the status of the service unit

Notes:
  • The <username> should be your @HOME address.
  • The “user” in the pathname for the service unit file is literally the string “user.”
  • The naming of the service and timer can be the same name except for the extension. It will ensure that the files will automatically find each other without having to reference the filenames explicitly. The extension for the service unit file should be .service, while the extension for the timer unit file should be .timer.
  • The description in the [Unit] section explains the service.
  • The ExecStart option in the [Service] section sets the command to run and should provide an absolute address with no variables. For example, we specify /home/tuts/venv/bin/python /home/tuts/birthday_countdown_app.py as the full path of the virtual environment and the python script file.
  • An exception to the absolute addresses for user units is “%h” for $HOME. So, for example, you can use:
    %h/venv/bin/python %h/birthday_countdown_app.py
  • Substituting %h for $HOME is only recommended for user unit files, not system units. This is because system units will always interpret “%h” as “/root” when run in the system environment.
  • The [Type] option is set to oneshot, which tells the systemd to run our command and that the service is not to be considered “dead” just because it finishes.

Create a systemd timer unit

The next step is to create a .timer unit file that schedules the .service unit. Create it with the same name and location as your .service file.

$ sudo nano /etc/systemd/user/birthday_countdown.timer
Countdown timers
[Unit]
Description=Schedule a message every 1 hour
RefuseManualStart=no # Allow manual starts
RefuseManualStop=no # Allow manual stops 

[Timer]
#Execute job if it missed a run due to machine being off
Persistent=true
#Run 120 seconds after boot for the first time
OnBootSec=120
#Run every 1 hour thereafter
OnUnitActiveSec=1h
#File describing job to execute
Unit=birthday_countdown.service 

[Install]
WantedBy=timers.target

Notes:
  • The description in the [Unit] section explains the timer.
  • Use RefuseManualStart and RefuseManualStop to allow manual starts and stops.
  • Use Persistent=true so that the service is triggered on the next boot if it was scheduled to run in a period the server is shutdown or instances when there is a network or server failure. Note, the default is always false.
  • OnBootSec= refers to the time since a system boot. You can also use OnStartupSec=, which refers to the time since service manager startup.
  • Use OnUnitActiveSec= to trigger the service at a specif time after the service was last activated. You can also use OnUnitInactiveSec= to specify a time after the service was last deactivated.
  • Use Unit= to specify the .service file describing the task to execute.
  • The [Install] section lets systemd know that timers.target want the timer that activates the boot timer.
  • In the example above, the service will run 120 seconds after boot and run after every 1 hour after that.
OnCalendar

You can also specify the schedule using OnCalendar, which is much more flexible and straightforward.

[Unit]
Description=Schedule a message daily
RefuseManualStart=no # Allow manual starts
RefuseManualStop=no # Allow manual stops 

[Timer]
#Execute job if it missed a run due to machine being off
Persistent=true
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
Unit=birthday_countdown.service

[Install]
WantedBy=timers.target
Notes:
  • OnCalendar is using daily to run the service at midnight. However, for more flexibility, the RandomizedDelaySec=1h instructs the systemd to choose a launch at a random time within 1 hour of midnight. RandomizedDelaySec can be essential if you have many timers running with OnCalendar=daily.
  • You can also check out systemd time span abbreviations which can let you denote 3600 seconds as 1h and so on.

Enable the user service

Enable the user service to test the service you created and make certain everything works.

$ systemctl --user enable birthday_countdown.service
  Created symlink /home/tuts/.config/systemd/user/timers.target.wants/birthday_countdown.service 
   → /etc/xdg/systemd/user/birthday_countdown.service.

Test the service with the following command:

$ systemctl --user start birthday_countdown.service

Check the output file ($HOME/bc.txt) to make sure the script is performing correctly. There should be a single entry message “It is your birthday in x days.”

Text File output
Text File output [bc.txt]

Enable and start the timer

Once you have tested the service, start and enable the service with the following commands:

$ systemctl --user enable birthday_timer.timer 
  Created symlink /home/tuts/.config/systemd/user/timers.target.wants/birthday_countdown.timer 
   → /etc/xdg/systemd/user/birthday_countdown.timer
$ systemctl --user start birthday_timer.timer

Enable and start commands prompts timer to start the service when scheduled.

$ systemctl --user status birthday_countdown.timer

status timer unit
Check status timer unit.

After letting the timer run for a few hours, you can now check the output file ($HOME/bc.txt). There should be several lines with the message “It is your birthday in x days.”

Text file output
Text File output [bc.txt]

Other essential operations

Check and monitor the service and debug error messages from the service unit:

$ systemctl --user status birthday_countdown
$ systemctl --user list-unit-files

Manually stop the service:

$ systemctl --user stop birthday_countdown.service

Permanently stop and disable the service and timer:

$ systemctl --user stop birthday_countdown.timer
$ systemctl --user disable birthday_countdown.timer
$ systemctl --user stop birthday_countdown.service
$ systemctl --user disable birthday_countdown.service

Reload the config daemon:

$ systemctl --user daemon-reload

Reset failure notifications:

$ systemctl --user reset-failed

Scheduling tips and tweaks

Calendar expressions

OnCalendar expressions make it simple and give you more flexibility in scheduling timers and services.

The following examples illustrate some typical time schedules you can specify.

On the minute, of every minute, every hour of every day:

OnCalendar=*-*-* *:*:00

On the hour, every hour of every day:

OnCalendar=*-*-* *:00:00

Every day:

OnCalendar=*-*-* 00:00:00

10 am daily:

OnCalendar=*-*-* 08:00:00

Weekdays at 6 am on U.S East Coast:

OnCalendar=Mon..Fri *-*-* 02:00 America/New_York

At midnight on the first day of every year:

OnCalendar=*-01-01 00:00:00 UTC

Midnight on the first day of every year in your timezone:

OnCalendar=*-01-01 00:00:00 or OnCalendar=yearly

To run on 10:10:10 of the third or seventh day of any month of the year 2021, but only if that day is a Monday or Friday.

OnCalendar=Mon,Fri 2021-*-3,7 10:10:10

Notes:

  • In the examples above, * is used to denote “every.” It could denote every date, every time, and timezone.
  • OnCalendar also provides the minutely, daily, hourly, monthly, weekly, yearly, quarterly, or semiannually shorthand expressions.
  • Use timedatectl list-timezones to list possible timezones.

systemd-analyze calendar

systemd-analyze calendar allows you to test any of your time schedules before you specify on OnCalendar=.

For example, check the validity of a service scheduled to run every Monday, Thursday, and Friday at 10 pm UTC.

systemd-analyze calendar "Mon,Thu,Fri *-1..11-* 22:00 UTC"

Next, list several iterations when the service is to run:

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

Check several iterations in a specific calendar year with the –base-time option:

systemd-analyze calendar --base-time=2022-01-01 --iterations=12 "Mon,Wed,Fri *-1..11-* 23:00 UTC"

Once your calendar test expression checks out OK, you can now confidently set OnCalendar= to your desired schedule.

Further reading:
Check out these official documentation and man pages for more details and tweaks on mastering systemd timers.

  • man systemd.timer
  • man systemd.service
  • systemd: A practical tool for sysadmins
  • systemd-analyze

Summary

The article introduces systemd timers and how to schedule system jobs as an alternative to cron. The structure of a .service and .timers unit files, defining timer schedules with countdown timers and calendar expressions via keywords like OnBootSec= or OnCalendar=. Finally, we highlighted how to troubleshoot calendar expression with systemd-analyze, proper systemctl operations, and some handy scheduling tips to guide you along the way.

I use systemd timers, but if you fancy cron, look at our intro guide on scheduling jobs with cron.

Leave a comment

Your email address will not be published. Required fields are marked *