โ† back to journal

Three Schedulers, One Missing Hour

2026-10-09 ยท Kit Wickham

Put a nightly job at 02:30 New York time. On 14 March 2027 the clocks jump from 02:00 straight to 03:00, so 02:30 never happens. Classic cron runs the job at 03:00. A systemd timer skips it until the next night. A Kubernetes CronJob skips it too. Then on 7 November the clocks go from 02:00 back to 01:00, and a job at 01:30 runs once under cron and systemd, and twice under Kubernetes, an hour apart.

None of the three is broken. They just picked different answers to a question most people only meet when a backup goes missing. I ran all three through both 2027 clock changes in New York and London to see what they actually do.

How I tested

Everything ran in a Debian 13 container with tzdata 2026c.

The nights: New York skips 02:00โ€“02:59 on 14 March and repeats 01:00โ€“01:59 on 7 November. London skips 01:00โ€“01:59 on 28 March and repeats 01:00โ€“01:59 on 31 October. New York and London gave the same pattern, so the table uses New York times.

What happened

Jobcron (cronie)systemd timerKubernetes CronJob
Daily 02:30, spring night (02:30 doesn't exist)Runs at 03:00Skipped, next run the following nightSkipped
Daily 01:30, autumn night (01:30 happens twice)OnceOnce (the first)Twice, an hour apart
Every 30 minutes, autumn nightRuns through both 01:00 hoursSkips the second: 90 minutes between runsRuns through both 01:00 hours
Hourly, autumn nightRuns in bothSkips the second: 2 hours between runsRuns in both
Every 30 minutes or hourly, spring nightNormalNormalNormal

The spring row for interval jobs is "normal" because nothing is lost in real time: 01:30 EST is followed by 03:00 EDT, which is 30 minutes later.

Three different ideas of what a schedule means

cron treats a daily job as "once a day, around this time." cronie's manual says so directly: for clock changes of less than three hours, jobs at a specific time "that would have run in the interval that has been skipped will be run immediately", and when the clock goes back, "running the same job twice is avoided." Jobs that run more often than that are "scheduled normally", and in my test an hourly job (15 * * * *) was too, so they run through both copies of the repeated hour. A bigger change than three hours counts as a clock correction, and cron just uses the new time. For nightly jobs it's the most forgiving of the three.

systemd treats a calendar line as "the next moment the wall clock shows this." 02:30 never shows on the spring night, so the next match is 02:30 the following night. On the autumn night it takes the first 01:30, and even if you start the timer at 01:45 EDT, between the two, the next run is 01:30 the following day. The same rule hits interval timers: after firing at 01:30 EDT, the next time the clock reads a later :00 or :30 is 02:00 EST, so the second 01:00 hour is skipped entirely. An OnCalendar=hourly timer goes two real hours without firing.

Kubernetes (through robfig/cron) treats it as "every instant whose wall clock matches." It walks forward an hour at a time in real time. In spring, the 02:00 hour never comes up, so a 02:30 job is skipped. In autumn, 01:00 comes up twice, so a 01:30 job runs at 05:30 and 06:30 UTC. If that job isn't safe to run twice, like sending invoices or appending to a ledger, that's a real bug once a year.

What to do about it

Schedule in UTC when you can. UTC never changes its clocks, so all three schedulers behave the same. In Kubernetes, set .spec.timeZone: "Etc/UTC" (docs). Without the field, the schedule is read in the controller's local time zone, and putting TZ= or CRON_TZ= inside .spec.schedule is rejected. In systemd, add the zone to the line (systemd.time): OnCalendar=*-*-* 07:30 UTC. In cronie, put CRON_TZ=UTC above the jobs in the crontab.

If it has to follow local time, stay out of the change window. The US changes at 02:00 local time. The EU changes everywhere at once, at 01:00 UTC, which is 01:00 in London, 02:00 in Berlin and 03:00 in Helsinki. So in the US and Europe the skipped and repeated hours all fall between 01:00 and 04:00 local time, and a 04:30 job never notices. That's not true everywhere: Chile changes at midnight.

For "every N minutes" in systemd, use a monotonic timer. OnUnitActiveSec=30min counts elapsed time from the last run and doesn't care what the wall clock says, so it has no gap in autumn.

Make jobs safe to run twice anyway. Even cron's "avoid running twice" only holds for clock changes under three hours, and the Kubernetes docs say a CronJob can create two Jobs, or none, for one scheduled time in other situations too, and that Jobs should be idempotent.

My own cron.wickkit.cc, which lists the next runs of a crontab line in a time zone, follows systemd's rule: a skipped time doesn't run, a repeated time runs once. That's right for a single daily job in systemd and wrong for cronie and Kubernetes in different ways, and a list that shows only one answer hides that. Showing all three side by side is the next thing I'm adding.

Update: it's there now. Below the list of next runs, the page shows the next two clock changes in your zone and every time within three hours of each where the three schedulers differ. */30 in New York shows the hour systemd drops; 30 2 * * * shows cronie's catch-up run.