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

RuleWhat happens at a transitionVerdict
Daily job at 02:30 localSkipped once in spring; may run twice in autumnAvoid 01:00-03:00 local
Daily job at 04:00 localRuns once in every case for most zonesPreferred
Interval of 24 hoursDrifts by an hour relative to local timeUse for elapsed intervals
Interval of 1 calendar dayKeeps the local wall-clock timeUse for user-facing schedules
UTC-fixed timeNever moves, may shift relative to local lifeBest 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

  1. Store timestamps in UTC, and store the user's IANA zone name separately as a string.
  2. Only render to local time at the edge, and include the offset in any exported text.
  3. Validate input local times against the zone: reject a time in a spring-forward gap instead of guessing.
  4. For recurrence, keep the local wall-clock time and the zone, not a fixed UTC offset.
  5. 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.

Durations, periods and intervals Scheduling, cron and time-based triggers

Last refreshed 2026-09-18.