Systemd Timer: A Tested Guide to OnCalendar and Persistent
Sagar Tayde · · 6 min read
A systemd timer runs a program on a schedule, like a cron job, but it is managed by systemd. You get logs in the journal, protection against overlapping runs, and an option to catch up on runs the machine missed while it was off.
This guide walks through a working example, the OnCalendar syntax, and three behaviours we measured on
systemd 259 (Ubuntu): how accurate the start time is, what happens when a run is slow, and how
Persistent= catches up.
On this page
- How it works: two files
- A complete example
- OnCalendar syntax
- Timezones in OnCalendar
- What we measured
- Commands you will use
- Timers compared with cron
- FAQ
How it works: two files
A timer is always a pair of unit files with the same name:
backup.servicesays what to run.backup.timersays when to run it.
When the timer elapses, systemd starts the service. Because the service is an ordinary unit, you can
also run it by hand with systemctl start backup.service to test it, without waiting for the schedule.
A complete example
A nightly backup at 2 am. Put both files in /etc/systemd/system/:
# backup.service
[Unit]
Description=Nightly database backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# backup.timer
[Unit]
Description=Run the backup every night at 2 am
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Then load and enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
Enable the timer, not the service. The service has no [Install] section on purpose: it should only
start when the timer fires.
OnCalendar syntax
The format is DayOfWeek Year-Month-Day Hour:Minute:Second, and any part can be *. Check an expression
before you use it with systemd-analyze calendar. These were all checked that way:
| You want | OnCalendar |
|---|---|
| Every day at midnight | daily (same as *-*-* 00:00:00) |
| Every day at 9 am | *-*-* 09:00 |
| Every 5 minutes | *:0/5 |
| Weekdays at 9:30 am | Mon..Fri 09:30 |
| Mondays at 2 am | Mon *-*-* 02:00 |
| 1st of every month at 2 am | *-*-01 02:00 |
systemd-analyze calendar --iterations=2 'Mon..Fri 09:30'
It prints the normalised form and the next run times, which catches most mistakes. For a schedule
relative to boot or to the last run instead of the clock, use OnBootSec= or OnUnitActiveSec=.
Timezones in OnCalendar
A plain OnCalendar uses the machine’s timezone, which is usually UTC on a cloud server. You can also
add a timezone name at the end. We checked both of these with systemd-analyze on a UTC machine:
| OnCalendar | Next run, in UTC |
|---|---|
*-*-* 09:00 Asia/Kolkata |
03:30 |
*-*-01 02:00 Asia/Kolkata |
20:30 on the last day of the previous month |
The second line matters. With standard cron on a UTC server, “2 am IST on the 1st” cannot be written, because it falls on a different date each month in UTC. Our cron timezone guide explains that trap. A timer with a timezone name handles it.
What we measured
We ran three tests with user timers on systemd 259. Each one wrote a timestamp to a file whenever its service started and ended.
1. Start accuracy. systemd’s documentation says the default AccuracySec=1min lets it delay a start
by up to a minute, to group wake-ups together. On our idle machine, timers with the default and with
AccuracySec=1s both started within one second of the minute. If you need the exact second, set
AccuracySec=1s anyway, because a busier machine may not behave like ours.
2. Slow runs do not overlap. A timer every 10 seconds started a job that took 25 seconds. In 60 seconds it started 3 runs, back to back: at 0, 25 and 50 seconds. A new run began the moment the last one ended, and two never ran at once.

Cron behaves differently. It starts a new copy at every match, so the same job would have had up to three
copies running together. That is why cron jobs need flock, as our
cron troubleshooting checklist explains.
3. Persistent catches up once. We stopped a 10-second timer for 35 seconds, which skipped three runs,
then started it again. With Persistent=false, nothing ran until the next scheduled time. With
Persistent=true, one run started 0.1 seconds after the restart, not three.
So Persistent=true is the setting for a laptop or a server that reboots: a daily job missed overnight
runs once at boot. It also means a daily job can run soon after every boot if the machine is often off at
the scheduled time, which surprises some people.
Commands you will use
systemctl list-timers --all # every timer, last and next run
systemctl status backup.timer # is it enabled and waiting?
journalctl -u backup.service --since today # output of the runs
sudo systemctl start backup.service # run the job now, by hand
sudo systemctl disable --now backup.timer # stop scheduling it
For a timer that belongs to your own user, put the files in ~/.config/systemd/user/ and add --user
to each systemctl command. A user timer only runs while you are logged in, unless lingering is enabled
with loginctl enable-linger.
Timers compared with cron
| Cron | systemd timers | |
|---|---|---|
| Files per job | One crontab line | A .service and a .timer |
| Output | Mailed or lost unless redirected | In the journal |
| Overlapping runs | Starts a new copy every time | Does not start a second copy |
| Missed runs while off | Lost | Caught up once with Persistent=true (OnCalendar timers only) |
| Timezone per job | Only with CRON_TZ on some crons |
A zone name in OnCalendar |
| Check a schedule | By eye | systemd-analyze calendar |
Cron is still quicker for a one-line job. Timers are better when the job matters: you get its output, no overlap and a catch-up after downtime. For the basics of cron itself, see what a cron job is.
The full option list is in the systemd manual pages for systemd.timer and calendar expressions.
Scheduled jobs on Voroa
On a hosting platform you usually do not get systemd at all: your app runs in a container. On Voroa, a Cron Job or Scheduled Task takes a cron expression, read in India Standard Time, and runs your command inside a service that stays running.
It behaves more like cron than a timer. A slow run can overlap the next, and runs missed while the service is stopped are skipped, not caught up. So keep jobs safe to run twice. See scheduling a task on Voroa and worker or cron job. Programs that should run all the time belong on the background workers page.
FAQ
How do I list systemd timers?
Run systemctl list-timers --all. It shows each timer with its last and next run.
Do I enable the timer or the service?
The timer. Enabling it with systemctl enable --now name.timer makes it start at boot and begin
scheduling.
How do I run a timer every 5 minutes?
Use OnCalendar=*:0/5, or OnUnitActiveSec=5min for every 5 minutes after the last run.
What does Persistent=true do?
If the machine was off when a run was due, the job runs once as soon as the timer starts again, for
example at boot. It works only with OnCalendar= timers, and does nothing on a timer’s very first start.
Can a systemd timer run twice at the same time? No. In our test a slow job was started again only after the previous run ended.
Which timezone does OnCalendar use?
The machine’s timezone, unless you add a zone name such as Asia/Kolkata at the end of the expression.
Tested on systemd 259 (Ubuntu) on 7 November 2026. Found something out of date? Write to [email protected].