Daylight saving and its edge cases
Spring-forward gaps, fall-back repeats, the difference between a nonexistent and an ambiguous local time, and the scheduling rules that avoid double-running or skipping a job.
Two clocks move in opposite directions
When a zone enters daylight saving, the local clock jumps forward and a range of local times never happens. When it leaves, the clock jumps back and a range happens twice. Both cases break naive assumptions.
Europe/Paris, 2026
spring forward, 29 March 02:00 -> 03:00
01:58 01:59 03:00 03:01 (02:30 does not exist)
fall back, 25 October 03:00 -> 02:00
02:58 02:59 02:00 02:01 (02:30 happens twice)from datetime import datetime
from zoneinfo import ZoneInfo
paris = ZoneInfo("Europe/Paris")
# nonexistent local time: resolved by a rule, not by an error
gap = datetime(2026, 3, 29, 2, 30, tzinfo=paris)
print(gap.utcoffset()) # +01:00, so it lands at 03:30 local
# ambiguous local time: fold selects which occurrence
a = datetime(2026, 10, 25, 2, 30, tzinfo=paris, fold=0) # earlier, +02:00
b = datetime(2026, 10, 25, 2, 30, tzinfo=paris, fold=1) # later, +01:00
print(a.utcoffset(), b.utcoffset())The fold attribute is how Python represents the ambiguity. JavaScript has no equivalent, which is one reason to prefer a library that does.
Scheduling rules that survive a transition
| Rule | What happens at a transition | Verdict |
|---|---|---|
| Daily job at 02:30 local | Skipped once in spring; may run twice in autumn | Avoid 01:00-03:00 local |
| Daily job at 04:00 local | Runs once in every case for most zones | Preferred |
| Interval of 24 hours | Drifts by an hour relative to local time | Use for elapsed intervals |
| Interval of 1 calendar day | Keeps the local wall-clock time | Use for user-facing schedules |
| UTC-fixed time | Never moves, may shift relative to local life | Best for infrastructure jobs |
# cron in local time is zone-dependent
30 2 * * * # in Europe/Paris this is skipped on 29 March
# cron in UTC has no gaps
30 1 * * * # 01:30 UTC is a real instant on every day- Cron entries such as
30 2 * * *in a local zone are skipped on spring-forward days by most schedulers. - Autumn fall-back can run a job twice unless the scheduler deduplicates by the intended instant.
- Choose a time outside the transition window, or express the schedule in UTC.
Practical rules
- Store timestamps in UTC, and store the user's IANA zone name separately as a string.
- Only render to local time at the edge, and include the offset in any exported text.
- Validate input local times against the zone: reject a time in a spring-forward gap instead of guessing.
- For recurrence, keep the local wall-clock time and the zone, not a fixed UTC offset.
- Test with a real transition date in a zone that has one. US, EU and AU transitions fall on different dates, and some zones shift by 30 minutes, not 60.
💡
Lord Howe Island shifts by 30 minutes. Nepal is UTC+05:45. Some zones have changed their DST rules more than once. This is why you must load the tz database and keep it updated rather than hardcoding offsets.
FAQ
How do I detect a nonexistent local time?
Round-trip it: convert the local time to UTC and back, and compare. If the result differs, the input was in a DST gap.
Why did my daily report skip a day?
It probably runs at a local time inside the spring-forward gap. Move it an hour later, or schedule it in UTC.
Related
Durations, periods and intervals Scheduling, cron and time-based triggers
Last refreshed 2026-09-18.