Daylight Saving Ends 25 October (EU) and 1 November (US). Know What Your 01:30 Scheduled Job Does
A job pinned to local time can run once, twice or late on a clock-change day. Which one depends on the scheduler, not on your code.
Daylight saving ends in the EU on Sunday 25 October 2026 and in the US on Sunday 1 November. If you run a scheduler anywhere that talks to either, there are two nights in the next two weeks when a job set for 01:30 local time can behave differently from every other night of the year.
Most daylight saving cron jobs work fine for years and then fail in one of three ways on a single night. This article lays out the three, shows what the clock arithmetic actually does with a few lines of Python, and gives a rule for deciding which jobs belong in UTC and which do not.
Daylight saving cron jobs fail in three different ways
A clock change creates two kinds of awkward local time. When clocks go forward, a stretch of local time never happens: in New York on 8 March 2026, the clock jumped from 02:00 to 03:00, so 02:30 did not exist. When clocks go back, a stretch happens twice: on 1 November 2026, 01:30 occurs once in daylight time and again an hour later in standard time.
A scheduler that takes "run at 02:30" has to pick a behaviour for each case. The common outcomes are skip the job, run it once at the next valid moment, or run it twice. Each is a defensible policy. The failure is not knowing which one you have.
- Skipped: the nightly job does not run, and the gap shows up days later in a report.
- Shifted: the job runs an hour late, which collides with whatever now runs at the real 03:00.
- Doubled: the job runs twice, and anything that is not idempotent writes its output twice.
What the clock change does to local time
The cleanest way to see it is to ask the standard library. This snippet uses Python 3.9+ zoneinfo with America/New_York and the two 2026 transition dates.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")
# Spring forward, 8 March 2026: 02:30 does not exist
t = datetime(2026, 3, 8, 2, 30, tzinfo=ny)
print(t.astimezone(timezone.utc)) # 2026-03-08 07:30:00+00:00
print(t.astimezone(timezone.utc).astimezone(ny)) # 2026-03-08 03:30:00-04:00
# Fall back, 1 November 2026: 01:30 happens twice
for fold in (0, 1):
t = datetime(2026, 11, 1, 1, 30, tzinfo=ny, fold=fold)
print(fold, t.astimezone(timezone.utc))
# 0 -> 2026-11-01 05:30:00+00:00 (first, daylight time)
# 1 -> 2026-11-01 06:30:00+00:00 (second, standard time)Two things are worth noticing. The nonexistent 02:30 is accepted without complaint and quietly becomes 03:30 when you convert through UTC and back. And the repeated 01:30 is ambiguous until you pick a fold, which defaults to the first occurrence. Libraries differ on whether they raise, shift or guess, so a schedule that never hits these cases in testing can go years before it meets one.
The same arithmetic changes the length of a day. Midnight to midnight in New York was 23 hours on 8 March and will be 25 hours on 1 November, which I confirmed by differencing the two UTC instants. Anything labelled "per day" inherits that: a daily quota, a daily revenue figure, a rolling 24-hour window that is not the same thing as a calendar day.
What classic cron does, and what it leaves out
Vixie-derived cron, the version most Linux distributions ship, has special handling for small clock changes. The Debian cron(8) manual page describes the case: when the clock moves forward by under three hours, jobs that fell in the skipped time run soon after the change, and when it moves back, jobs in the repeated time are not re-run. Only jobs set for a particular time are affected. Jobs with a wildcard in the hour or minute field, or @hourly, are scheduled normally and keep ticking.
That is a reasonable policy, and it is also specific to that implementation. Other cron variants, container schedulers, workflow engines and application libraries each make their own choice, and some do not document it. Kubernetes CronJobs, for instance, only gained a first-class timeZone field in recent releases, and before that they ran in the time zone of the controller manager. Check the version you actually operate rather than assuming the documentation you remember applies.
Why "run it in UTC" is half an answer
UTC has no transitions, so a job pinned to 07:30 UTC runs exactly once every 24 hours forever. That is the right answer for anything a machine cares about: backups, log rotation, cache warming, certificate checks, queue sweeps. Nobody needs a backup to happen at the same wall-clock time in Ohio all year.
It is the wrong answer when a person has a wall-clock expectation. A digest promised for 9 AM in Chicago should arrive at 9 AM in Chicago in January and in July, which means 15:00 UTC in one and 14:00 UTC in the other. Converting that to a fixed UTC time moves the delivery an hour twice a year, and the customer notices.
Decide per job: UTC anchor or wall-clock anchor
The useful question for each scheduled job is who or what depends on its timing. If the answer is a machine or an elapsed-time guarantee, anchor to UTC. If the answer is a human, a contract or a regulator, anchor to a wall clock in a named zone and write down what happens to the skipped and repeated hours.
| Job | Anchor | Why |
|---|---|---|
| Backups, log rotation, queue sweeps | UTC | Elapsed time matters, wall clock does not |
| Rate-limit and quota resets | UTC | Customers need a stable 24-hour rule, not a 23 or 25 hour one |
| "9 AM" digest or reminder email | Recipient zone | The promise is a wall-clock time |
| End-of-day reports for a branch or store | Local zone | The business day follows the wall clock, so it is 23 or 25 hours twice a year |
| Statutory cut-offs and filing windows | Jurisdiction zone | The rule is written in local time; get the policy for skipped and repeated hours from the rule itself |
For wall-clock jobs, compute each occurrence from the date. The loop below produces one run per calendar day across both transitions, which is the property you want, and it makes the day-length change visible in the gaps.
from datetime import date, datetime, timedelta, timezone
from zoneinfo import ZoneInfo
def runs(start, days, hh, mm, zone):
z = ZoneInfo(zone)
for i in range(days):
d = start + timedelta(days=i)
yield datetime(d.year, d.month, d.day, hh, mm, tzinfo=z).astimezone(timezone.utc)
# Daily 02:30 New York across spring-forward:
# 03-07 07:30Z, 03-08 07:30Z, 03-09 06:30Z, 03-10 06:30Z (gaps: 24h, 23h, 24h)
# Daily 01:30 New York across fall-back:
# 10-31 05:30Z, 11-01 05:30Z, 11-02 06:30Z, 11-03 06:30Z (gaps: 24h, 25h, 24h)Note what the first block does on 8 March: the job still runs exactly once, shifted to 03:30 local, and the next gap is 23 hours. On 1 November it runs at the first 01:30 only. That is one policy, and a fine default. If your job must run in the repeated hour as well, or must never be shifted, say so explicitly in the code and in the runbook.
Store the zone name, not the offset
A stored offset such as -05:00 describes one moment in one place. It carries no information about what the offset will be in six months. Store the IANA zone name (America/New_York, Europe/London, Asia/Kolkata) with each schedule, and resolve it against the time zone database at run time.
Two related habits help. Keep the time zone database current on every machine and container image that schedules work, because governments change the rules with little notice. And log both the intended local time and the UTC instant for every run, so a missing or doubled execution can be explained from the log alone.
Test the transition dates before they arrive
You can check all of this in staging with no production risk. The goal is to watch each scheduled job cross a transition once, with someone looking.
- List every scheduled job and tag it UTC-anchored or wall-clock-anchored. Anything untagged is a finding.
- For wall-clock jobs, write down the intended behaviour for the skipped hour and the repeated hour.
- Set the test environment clock, or the scheduler's injectable clock, to a few minutes before 01:00 UTC on 25 October and again before the US transition, and watch each job.
- Make non-idempotent jobs safe to run twice, or give them a lock keyed on the intended local date.
- Add a missed-run alert so a skipped job is noticed within an hour, not by a customer the next week.
Closing thoughts
The time zone database is maintained by volunteers and changes several times a year, and schedulers keep multiplying as teams add workflow engines, queues and cloud timers on top of cron. The habit that scales is the same one this article started with: for every scheduled job, write down in one line whether it is anchored to the clock on the wall or to elapsed time, and the rest of the decisions follow from that.
Frequently asked questions
Related reading
Passkey Adoption Is 5 Billion Strong. 57% of Enterprises Still Sign In With Phishable Methods.
FIDO's 2026 report counts 5 billion passkeys, yet 57% of enterprises still rely on phishable primary sign-in. Why enrolment and removal are different metrics.
Retry Budgets Have Been the Fix for a Decade. GitHub's 8-Hour Outage Shows Why Teams Still Skip Them.
GitHub's August 17 outage stretched to nearly eight hours partly because retries amplified the failure they were meant to survive. The fix has existed for a decade. Most client libraries still skip it.
The PLC Vulnerability Behind the US Water Utility Attacks Has No Patch, and Never Will
A wave of attacks on US water utilities is running through a Rockwell PLC bug with no vendor fix. The advice to "apply the patch" doesn’t apply here, and that gap explains most of what OT security gets wrong.