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,
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.
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
- 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).