SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

systemd OnCalendar syntax: carry your cron line go timer

See how OnCalendar dey read weekday, date, time and zone. We carry common cron lines go timer, from every 15 minutes reach first Monday, plus one job wey run for Lagos time.

Di short answer

systemd OnCalendar syntax na four parts for one line: weekday, date, time, then timezone for di end. Weekday and timezone na optional. If you no write date, systemd go use *-*-*, wey mean every day. If you no write time, e go use 00:00:00, wey mean midnight. So di cron line 0 2 * * * turn OnCalendar=*-*-* 02:00, and 0 9 * * 1-5 turn OnCalendar=Mon..Fri 09:00.

Before you put any expression inside timer, run systemd-analyze calendar on top am. Di command go show you di normalized form, wey be di full way systemd read your line. If wetin e print no be wetin you mean, fix am there. No wait till 02:00 before you find out.

This guide assume say you don already get cron line or one job wey dey repeat for your VPS (virtual private server). We no go teach how to create di .service and .timer files from scratch. For dat one, follow how to run job with systemd service and timer on VPS, then come back here for di schedule part.

Which systemd version your server get?

Di grammar for this guide come from di systemd.time(7) man page. Di Ubuntu 24.04 page show systemd 255.4. Di Ubuntu 26.04 page show systemd 259.9. Di calendar grammar and di shorthands wey we use here dey di same for both pages. Check your own server:

systemd-analyze --version

Di first line go show di version number. If your server get another version, read man systemd.time for di box itself, because na di installed version dey decide wetin go work.

How di OnCalendar syntax take arrange

Di full form get this shape:

Weekday  Year-Month-Day  Hour:Minute:Second  Timezone

Di man page own example na Thu,Fri 2012-*-1,5 11:12:13. E mean 11:12:13 on di first or fifth day of any month for 2012, but only if dat day na Thursday or Friday. Look dat last part well. Di weekday and di date must agree together. This one go matter well well when we reach first Monday.

  • Weekday: English names, short (Mon) or full (Monday). Capital or small letter no matter.
  • Date: year-month-day. *-*-* mean any year, any month, any day.
  • Time: hour:minute:second. If you no write seconds, systemd use :00.
  • Timezone: di word UTC, or one IANA (Internet Assigned Numbers Authority) timezone name like Africa/Lagos. E dey go for di end.

Di symbols: star, comma, two dots and slash

Each part of di date or time fit carry these symbols:

  • * mean any value. *-*-* na every day.
  • , na list. 1,15 for di day place mean day 1 and day 15.
  • .. na range. Mon..Fri na Monday reach Friday. 12..14 for di hour place na 12, 13 and 14 o'clock.
  • /N na repetition. Di value itself match, plus di value plus every multiple of N. So 0/15 for di minute place match 0, 15, 30 and 45.
  • ~ mean count from di end of di month. Di man page example Mon *-05~07/1 mean di last Monday for May.

You fit mix , and .. for weekday as you like. Di man page show Sat,Thu,Mon..Wed,Sat..Sun, and systemd clean am to Mon..Thu,Sat,Sun *-*-* 00:00:00. E arrange di days, remove di ones wey repeat, and add di date and time wey you no write.

Di shorthands and wetin dem really mean

systemd get some short words. Plenty people think say daily mean 02:00 or one random time for night. No be so. Di man page give di full meaning:

  • minutely na *-*-* *:*:00
  • hourly na *-*-* *:00:00
  • daily na *-*-* 00:00:00, wey be midnight
  • weekly na Mon *-*-* 00:00:00, wey be Monday midnight
  • monthly na *-*-01 00:00:00
  • quarterly na *-01,04,07,10-01 00:00:00
  • semiannually na *-01,07-01 00:00:00
  • yearly and annually na *-01-01 00:00:00

If di time you want no be midnight, no use shorthand. Write di full expression.

Cron line to OnCalendar: di schedules wey people really need

Cron get five fields: minute, hour, day of month, month, day of week. OnCalendar no get fields like dat. E get di four parts wey we don see. Here na di schedules wey dey come up pass. Each one get di cron line first, then di OnCalendar line.

  1. Every 15 minutes. Cron: */15 * * * *. OnCalendar: *:0/15. Di crontab(5) page talk say step dey work only inside di field wey e dey, so cron */15 for di minute place run for minute 0, 15, 30 and 45 of every hour. systemd 0/15 for di minute place match di same four minutes. Di nearest example for di systemd man page na *:2/3, wey systemd write as *-*-* *:02/3:00.
  2. Every hour. Cron: 0 * * * *. OnCalendar: hourly, wey be *-*-* *:00:00.
  3. Every day for 02:00. Cron: 0 2 * * *. OnCalendar: *-*-* 02:00, or just 02:00. Di man page show 05:40 as *-*-* 05:40:00, so di short one work too.
  4. Monday reach Friday for 09:00. Cron: 0 9 * * 1-5. OnCalendar: Mon..Fri 09:00.
  5. Day 1 and day 15 of every month, midnight. Cron: 0 0 1,15 * *. OnCalendar: *-*-1,15. Since you no write time, systemd use 00:00:00. For 08:00, write *-*-1,15 08:00. Dis one good for salary reminder or mid-month invoice.
  6. First Monday of di month for 09:00. Cron: no clean one line (see di next section). OnCalendar: Mon *-*-1..7 09:00.

Di last row no be example from di man page. E come from di grammar: weekday Mon, plus day range 1..7, plus di rule say weekday and date must agree. Di first Monday must fall between day 1 and day 7, so only one day per month go match. Run am through systemd-analyze calendar for your server before you trust am, same as every other row.

Why cron no fit do first Monday for one line

Plenty people write 0 9 1-7 * 1 for cron and think say e mean first Monday. E no mean dat. Di crontab(5) page talk say if both di day-of-month field and di day-of-week field no be *, di command go run when either field match. Di page own example na 30 4 1,15 * 5: e run for 4:30 am on di 1st and 15th, plus every Friday.

So 0 9 1-7 * 1 run on day 1 reach day 7, and again on every Monday for di month. Dat one na ten or eleven runs for one month instead of one. If di job send report to customers, dem go receive am many times.

systemd no get dis OR rule. For systemd, di weekday na filter on top di date. Na why Mon *-*-1..7 give only one run per month. If your reason to leave cron na say di job no dey run at all, first read why your cron job no dey run. Di cause fit be di script itself, and di script go carry di same problem enter systemd.

Check every expression with systemd-analyze calendar

systemd-analyze calendar take di same input as OnCalendar=. E parse am, normalize am, and show when e go fire next. Run am for each line wey you plan to use:

systemd-analyze calendar '*:0/15'
systemd-analyze calendar 'Mon..Fri 09:00'
systemd-analyze calendar '*-*-1,15'
systemd-analyze calendar 'Mon *-*-1..7 09:00'

Put di expression inside single quotes. Without quotes, your shell fit expand * into file names before systemd see am.

Di output get these labels: Original form, Normalized form, Next elapse and From now. Di man page example for *-2-29 0:0:0 start with these two lines:

  Original form: *-2-29 0:0:0
Normalized form: *-02-29 00:00:00

Normalized form na di line wey you go read. E show di full date and time, with zeros added and lists arranged. If e no match wetin you mean, di expression wrong. Next elapse and From now dey change every time you run di command, because dem depend on di current time. No copy those two from any guide, including this one.

If systemd no fit understand di expression, di command go show error instead of di form lines. Fix am there before you touch di timer file.

Di 24.04 man page also describe --iterations=, wey show more than one next run, and --base-time=, wey calculate from another starting time. Confirm say your installed version get dem before you use dem:

systemd-analyze --help | grep -E 'iterations|base-time'

If grep print both names, both flags dey. Di 26.04 man page and di upstream page no list any --timezone option. Di timezone must enter inside di expression itself.

Put di expression inside your timer

Your .timer file carry di schedule for di [Timer] section:

[Unit]
Description=Weekday morning report

[Timer]
OnCalendar=Mon..Fri 09:00
Persistent=true

[Install]
WantedBy=timers.target

After you save am, reload systemd and enable di timer. These ones you go run for your own VPS:

sudo systemctl daemon-reload
sudo systemctl enable --now report.timer
systemctl list-timers

Look for report.timer for di list. If e no dey there, check di file name and di [Install] section. Di service wey di timer start na usually job wey run and finish. Di systemd service types explain which Type= fit dat kind job.

Run one job for Lagos time while di server dey for UTC

Plenty servers dey keep their clock for UTC (Coordinated Universal Time). But your customers and your team fit dey Lagos. You no need change di whole server. Add di IANA zone name for di end of one expression:

OnCalendar=*-*-* 02:00 Africa/Lagos

Dis one mean 02:00 for Lagos clock, no matter wetin di server clock be. Check am:

systemd-analyze calendar '*-*-* 02:00 Africa/Lagos'

Read di Normalized form line. For di man page examples, di zone stay for di end of di normalized form: weekly Pacific/Auckland turn Mon *-*-* 00:00:00 Pacific/Auckland. Your Lagos line suppose follow di same pattern. Run am yourself and see wetin your server print.

WAT (West Africa Time) na di zone wey Africa/Lagos use. As of October 2026, di IANA tz database give Africa/Lagos one fixed offset of 1:00, abbreviation WAT, and no daylight saving rule. Di file note say standard time change to GMT+1 for 1919. So WAT na UTC+1 all year. 02:00 for Lagos na 01:00 for UTC, every day of di year. Di hour no dey shift.

Dis no-shift thing get value. Di systemd.time(7) page no talk anything about daylight saving. For zone wey get daylight saving, one hour fit no exist one day for di year, and another hour fit happen two times. If you use such zone, test di dates around di change well. For Africa/Lagos, dat problem no dey.

Zone for one timer, or zone for di whole server?

Di zone inside di expression touch only dat one timer. If you change di server timezone, you change di local time for everything wey read am, including cron jobs and every timer wey no carry zone. Di systemd.time(7) page talk say timestamp wey no carry zone dey use di current timezone. So one change for server level fit move jobs wey you no plan to move.

If you really want di whole box on Lagos time, follow how to set di timezone for Linux server. Else, keep di server for UTC and put Africa/Lagos only for di few timers wey need am. Your logs stay for one clock, and each schedule still read like Lagos time.

Why di timer fire small late: AccuracySec

You set 09:00 and di job start some seconds after. Nothing spoil. Di systemd.timer(5) page for both 24.04 and 26.04 talk say AccuracySec= default na 1min. Di timer go fire inside one window wey start for di time you set, and di window long reach AccuracySec. systemd choose one position inside dat window wey stay di same for all di local timers, so e fit wake di system once for many timers at di same time.

If di job really need to start for di exact second, make di window small. Di man page talk say for best accuracy, set am to 1us:

[Timer]
OnCalendar=Mon..Fri 09:00
AccuracySec=1us

Most backup and report jobs no need dis. One minute late no mean anything to backup wey run for 02:00.

When to use RandomizedDelaySec

RandomizedDelaySec= default na 0, wey mean no random delay. When you set am, systemd delay di timer by one random amount between 0 and di value wey you give. E dey pick new random delay before each run.

Use am when many servers get di same schedule. Imagine ten VPS wey all send backup to di same storage for 02:00 Lagos time. All of dem go start upload for di same second. With RandomizedDelaySec=15min, each server pick different moment between 02:00 and 02:15:

[Timer]
OnCalendar=*-*-* 02:00 Africa/Lagos
RandomizedDelaySec=15min
Persistent=true

No use am for job wey must start on time, like report wey somebody dey wait for by 09:00.

When every 15 minutes after di last run beat calendar

*:0/15 follow di wall clock. Di runs land on :00, :15, :30 and :45, no matter when di last one start. Sometimes wetin you want na 15 minutes after di last run. For dat one, use monotonic timer. Monotonic timer no follow wall clock.

  • OnBootSec= count from when di machine boot.
  • OnUnitActiveSec= count from when di unit wey di timer start last activate.
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min

Dis one run di job 5 minutes after boot, then 15 minutes after each time di service activate. Di 24.04 man page talk say these timers dey independent of wall-clock time and timezones. So Africa/Lagos no mean anything here, and if somebody change di server timezone, di gap no go move.

Use calendar when di time of day matter, like report for 09:00. Use monotonic when di gap between runs matter, like check for queue every 15 minutes. Timer na only for schedule. If you want systemd to try again when di service fail, dat one na systemd restart policy work, no be timer work.

Persistent=true: di run wey you miss when di server off

Persistent= default na false. If you set am to true, systemd store di last time di service run for disk. When di timer start again, e go run di job once if e miss any run while di server dey off. Turn am on for backups and reports. Without am, if di VPS dey reboot when 02:00 reach, dat night backup no go happen.

FAQ

Wetin be di OnCalendar line for cron */15 * * * *?

Use OnCalendar=*:0/15. Di /15 mean minute 0 plus every multiple of 15, so e match minute 0, 15, 30 and 45 of every hour. Di date default to every day, and di seconds default to :00. Run systemd-analyze calendar '*:0/15' to see di normalized form on your own server.

Why my daily timer run for midnight and not 02:00?

Because daily na *-*-* 00:00:00 for di systemd.time(7) man page. Di shorthand always mean midnight. If you want 02:00, write di full expression: OnCalendar=*-*-* 02:00. Di same thing apply to weekly, wey mean Monday 00:00.

How I go write first Monday of di month for systemd?

Write OnCalendar=Mon *-*-1..7 09:00. For systemd, di weekday must agree with di date, and only one Monday fit fall between day 1 and day 7. Cron no work like dat: for 0 9 1-7 * 1, cron run when either di day or di weekday match, so di job run on day 1 reach 7 and on every Monday too. Check di systemd line with systemd-analyze calendar before you use am.

How I go run one timer for Lagos time when di server dey for UTC?

Put di IANA zone for di end of di expression: OnCalendar=*-*-* 02:00 Africa/Lagos. Africa/Lagos use WAT, wey be UTC+1 all year with no daylight saving, so 02:00 Lagos na always 01:00 UTC. Dis one change only dat timer. Di server clock and every other job stay as dem be.

Why my systemd timer start some seconds after di time wey I set?

AccuracySec= default na 1min, so di timer fire somewhere inside one minute window after di time you set. systemd do am so e fit wake di system once for many timers. If you need di exact second, add AccuracySec=1us for di [Timer] section.