Scheduling bugs cluster around two moments in the year, and they are the same two moments for everyone: the second Sunday in March and the last Sunday in October — give or take, depending on which hemisphere and which government.
The assumption that breaks
The bug is almost always this: storing a meeting as a wall-clock time plus an offset rather than as an instant plus a timezone.
{ "startsAt": "2026-10-24T09:00:00+01:00", "recurrence": "WEEKLY" }
That looks unambiguous, and for a single meeting it is. But the recurrence rule
has to answer “when is the next one?” — and on 25 October, +01:00 stops being
the correct offset for Europe/London. Naively adding seven days gives you
2026-10-31T09:00:00+01:00, an instant that renders as 8:00am to the host.
Their weekly standup silently moves an hour earlier, and only for the half of
the team in a country that just changed its clocks.
The fix is to store the zone, not the offset:
{ "startsAt": "2026-10-24T09:00:00", "timeZone": "Europe/London", "recurrence": "WEEKLY" }
Now “next week at 9am” is a question about local time, resolved against the IANA database at read time, and it stays 9am through the transition.
The three rules we hold to
Store instants in UTC; store intent in IANA zones. A booked meeting has both: the UTC instant it occurs, and the zone the host expressed it in. You need the first to sort and the second to explain.
Never persist an offset as a zone. +05:30 is not Asia/Kolkata. It’s a
fact about one moment that happens to be true for that zone right now. Offsets
are output, never storage.
Resolve availability in the host’s zone, render in the guest’s. A host who says “I’m free 9–5” means their 9–5. Compute the window there, convert to instants, then present those instants wherever the guest happens to be. Doing it the other way — converting the rule itself — is how you end up with availability that shifts by an hour twice a year.
Some zones have offsets that aren't whole hours — Asia/Kolkata at
+05:30, Australia/Eucla at +08:45, Asia/Kathmandu at
+05:45. If your slot generator steps in whole hours from midnight UTC, none of
those users will ever see a slot on a round local time.
The nonexistent hour
The last one catches almost everybody. When clocks spring forward, a local time simply does not exist:
In America/New_York on 8 March 2026, the instant 02:30 never occurs — the
clock jumps 01:59:59 → 03:00:00. If a host has a recurring 2:30am slot, or more
plausibly if your code does date arithmetic that lands there, converting that
local time to an instant is undefined. Different libraries resolve it
differently: some throw, some shift forward an hour, some shift back.
And in autumn the inverse happens — 01:30 occurs twice, an hour apart, and
“which one?” is a real question with two correct answers.
BunnyCal resolves nonexistent times forward and ambiguous times to the first occurrence, and — more importantly — never generates a bookable slot inside either window. A slot nobody can reliably attend isn’t worth the edge case.
Testing this without waiting for October
Freeze the clock. Every timezone test we have pins a specific instant and a specific zone, and the DST ones pin instants either side of a transition that has already happened. You do not need to wait for the calendar to test the calendar.