Replacing cron jobs with systemd timers
Cron jobs are ubiquitous. They’ve been available since practically forever on every Linux machine. However, cron has some annoying shortcomings:
- Configuration is obtuse. The syntax for timing is hard to memorize, and some scenarios are downright impossible to implement - for example, a job that runs every 5 minutes of system uptime.
- Logging is difficult. Cron uses mail to report job status, and munches the output in the process. Want to log to journald? Cron will eat that too.
- We can’t see a history of cron job runs.
- Enabling and disabling cron jobs programmatically requires using scripted text-editing commands like “sed”.
systemd introduces a type of unit called timer
. These timers control a matching service. For example, foo.timer
starts foo.service
.
There are two types of timers:
- Realtime timers (also called wallclock timers) refer to real-world time, just like cron jobs.
- Monotonic timers are relative to events, such as machine boot or last activation time of the job. These are exciting because they let us configure true “every X minutes” jobs.
Configuring timers
To configure a timer we need to create two files - one is a .service
file that describes the job itself, and the other is the .timer
file which describes the timing of the job.
For example, let’s say we have this cron job that we want to migrate to systemd timers:
0 2 * * 1-5 /usr/bin/some-command
This cron roughly translates to “Run some-command
at 02:00 every day from Monday to Friday”.
To use systemd timers, we must first create a .service
file for the command we want to run:
# /etc/systemd/system/some-command.service
[Unit]
Description=Run some-command
Type=oneshot
[Service]
ExecStart=/usr/bin/some-command
Now we create the matching .timer
file - note that the timer and service files must have the same name:
# /etc/systemd/system/some-command.timer
[Unit]
Description=Run some-command every Monday to Friday at 02:00
[Timer]
OnCalendar=Mon..Fri 02:00
Persistent=true
[Install]
WantedBy=timers.target
Note that we use a shorthand notation for OnCalendar=
since we only care about the day of the week and the hour. The full form is Day-of-week Year-Month-Day Hour:Minute:Second
, so in our case it’s Mon..Fri *-*-* 02:00:00
. See the full reference for calendar timestamps.
We can also have multiple OnCalendar=
settings if we want to express a more advanced routine.
When we set Persistent=true
, systemd will launch the task if it missed the last start time but was unable to start it (due to the machine being down, for example). This effectively implements anacron
in systemd.
To enable the timed job, perform a daemon-reload
to make systemd read the new units, then enable the timer:
systemctl daemon-reload
systemctl enable --now some-command.timer
Configuring a monotonic timer
Sometimes we don’t really care at what time a job should be performed, but rather how often. In a monotonic timer we can set the interval of a job, and also when to run the job relative to the system’s startup.
To configure a monotonic timer we replace the OnCalendar=
setting with one or more of OnActiveSec=, OnBootSec=, OnStartupSec=, OnUnitActiveSec=, OnUnitInactiveSec=
.
For example, let’s make our task run 5 minutes after startup and every 1 hour after that:
# /etc/systemd/system/some-command.timer
[Unit]
Description=Run some-command every 1 hour, starting 5 minutes after system boot
[Timer]
OnBootSec=5min
OnUnitActivateSec=1h
[Install]
WantedBy=timers.target
Combining monotonic and realtime timers
What if we want our task to run on both a calendar event and an interval?
We can mix the monotonic settings with OnCalendar=
. The task will run when any of the timer expressions occurs. For example:
# /etc/systemd/system/some-command.timer
[Unit]
Description=Run some-command every 1 hour, starting 5 minutes after system boot
[Timer]
OnCalendar=Mon..Fri 02:00
OnBootSec=5min
OnUnitActivateSec=1h
[Install]
WantedBy=timers.target
Will run:
- 5 minutes after booting
- Every 1 hour from boot time
- At 02:00 on Mondays through Fridays
If we booted on a Friday at 01:30, the task will run at 01:35, 02:00, 02:35, 03:35 and every hour - until the following Monday when it will also run at 02:00.
Randomizing task runs
Sometimes we want to add a random delay before the task runs, in order to “fuzz” the timing.
For example, certbot adds a sleep
of up to one hour to the certificate renewal script, so it won’t be accessed by everyone at exactly :00 every hour.
The way it’s done in certbot is by using python to randomize a sleep of 0-3600 seconds.
0 0,12 * * * python -c 'import random; import time; time.sleep(random.random() * 3600)' && /usr/local/bin/certbot-auto renew
In systemd timers we can configure this delay directly:
[Unit]
Description=Renew certificates
[Timer]
OnCalendar=*-*-* 0,12:00
RandomizedDelaySec=3600
[Install]
WantedBy=timers.target
A quick note about accuracy
By default, timers in systemd have an accuracy of 1 minute. This means that a task that’s set to run at midnight will start at any time between 00:00:00 and 00:01:00. This is done to optimize power consumption and avoid unnecessary wake-ups.
If your task requires higher accuracy, you can configure the AccuracySec=
setting to any interval - down to 1us
(one microsecond).