DEV Community

Michael Maitland
Michael Maitland

Posted on

The timezone bugs you don't know you have (and how to find them)

A user reports that a meeting fired an hour late. You check the database — the timestamp looks fine. You check the UI — the display matches what the user entered. Then you realize: today was DST transition day.

This post is about the timezone bugs that 90% of production apps have and don't know about. None of these are exotic. All of them are quietly corrupting data in some app you've shipped.

Bug #1: The Spring-Forward Gap

On March 8, 2026 in America/New_York, clocks jump from 1:59:59 directly to 3:00:00. The entire 2:00–2:59 hour does not exist.

If a user enters "2:30am" in your scheduler:

const dt = DateTime.fromISO("2026-03-08T02:30:00", { zone: "America/New_York" });
console.log(dt.toISO());
// "2026-03-08T03:30:00.000-04:00"  ← Luxon silently shifted it to 3:30
Enter fullscreen mode Exit fullscreen mode

Luxon "helpfully" resolved the invalid time forward. Your user wanted 2:30am. You stored 3:30am. They have no idea. The bug surfaces 8 months later when their recurring event fires an hour late.

The fix: explicitly check whether a parsed datetime equals what you asked for, and reject (or escalate to the user) if it doesn't. Don't trust your library to "just handle it."

Bug #2: The Fall-Back Overlap

On November 1, 2026 in America/New_York, clocks go from 1:59:59 back to 1:00:00. The 1:00–1:59 hour happens twice. "1:30am" is genuinely ambiguous.

const dt = DateTime.fromISO("2026-11-01T01:30:00", { zone: "America/New_York" });
console.log(dt.offset);
// -240 (EDT) — but it could just as legitimately be -300 (EST)
Enter fullscreen mode Exit fullscreen mode

Most libraries pick one without telling you. Picking "the first occurrence" is a real business decision (your user might mean the second). It should not be a silent default.

The fix: when you detect an ambiguous local time, force a policy decision in your domain layer. Log it. Don't let the library guess.

Bug #3: Non-Whole-Hour Offsets

Pop quiz — what's the offset for Asia/Kathmandu? Most developers say "+5:30." Wrong, that's India.

  • Nepal: UTC+5:45
  • Chatham Islands: UTC+12:45
  • Lord Howe Island uses a 30-minute DST shift, not 60

If your code anywhere does offset / 60 to get hours, or assumes the minutes component is always 0 or 30, you have a bug. It might never fire, because you might never have a Nepali user. Until you do.

The fix: treat offsets as minutes-from-UTC, never as hours. Format defensively. Test with Kathmandu in your suite.

Bug #4: Historical Zone Rules

Germany on 1945-05-24 had a 23:00 → 01:00 jump (the occupation forces synchronized clocks). Russia in 2011 abolished DST permanently. Iran abolished DST in 2022. Egypt re-instated it in 2023. Samoa literally skipped December 30, 2011 to switch date lines.

If you're storing or computing historical timestamps — billing periods that started years ago, audit trails, IoT data from old devices — your results may be wrong by an hour for any user in an affected zone.

The fix: if you're computing across years, pin your tzdata version explicitly and document it. Don't assume the runtime's bundled tzdata is up to date.

Bug #5: tzdb Update Drift

The IANA tzdata gets updated 3-6 times per year. Every release fixes historical errors and adds new rule changes (countries change DST policy all the time — Mexico in 2022, Egypt in 2023, BC's pending change in 2026).

Your Node.js version was built with a snapshot of tzdata from whenever it was released. If you're on Node 20 from 2023, you're on tzdata from 2023. You're missing every IANA update since then.

This means your app gives wrong answers for any zone whose rules changed after your Node version's release date. Often by an hour, often silently.

The fix: decouple tzdata from your runtime version. Use a userland tzdb package you can update independently. I just did this for my production system and wrote up the migration if you want details.

How to test for these in your own code

For each of the bugs above, here's a one-liner test:

// Bug #1 detection
function isValidLocalDatetime(input, zone) {
  const dt = DateTime.fromISO(input, { zone });
  // Compare requested local time vs what the lib gave back
  return dt.toFormat("yyyy-MM-dd'T'HH:mm:ss") === input;
}

// Bug #2 detection
function isAmbiguousLocalDatetime(input, zone) {
  const dt = DateTime.fromISO(input, { zone });
  const before = dt.minus({ hours: 1 });
  const after = dt.plus({ hours: 1 });
  return before.offset !== after.offset;
}
Enter fullscreen mode Exit fullscreen mode

Run these against your top 50 customers' time zones for the next 5 years worth of DST transition dates. If you find a bug, congrats — you found one that probably already cost you a support ticket.

My take

I got tired of writing this code in every project. So I built ChronoShield API — a REST API that handles all of these cases as a service. Today's launch:

  • npm install chronoshield / pip install chronoshield
  • Free tier: 1k req/mo
  • Currently serving IANA tzdata 2026b (latest), with the BC permanent-DST projection already live
  • Endpoints: /validate, /resolve, /convert, /version

But the test cases above work whether or not you use the API. If you take nothing else from this post — write the tests. They'll find bugs you didn't know you had.

Top comments (0)