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
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)
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.
Bug #6: AI agents silently scheduling impossible times
You'd think that with five named bug classes documented above, AI tools would handle them carefully. They don't. The opposite, actually — AI agents make the prior five bugs worse, because they:
- Confidently parse natural language ("schedule me for 2:30 AM on March 8 in New York")
- Pass the parsed local time to a calendar, booking, or billing API without verification
- Get an answer back that looks fine
- Tell the user "done, I scheduled it for 2:30 AM"
- The downstream system silently shifts to 3:30 AM (Bug #1) or picks the wrong UTC instant (Bug #2)
There's no error. The agent doesn't know to second-guess itself. And the user trusts the agent's confirmation. By the time anyone notices, the meeting has already fired at the wrong hour, the cron job has already double-charged the customer, or the reminder has already failed to land.
I've watched Claude do exactly this. So has GPT-4. Both are perfectly happy to say "scheduled for 2:30 AM March 8, 2026" — a moment that does not exist in America/New_York.
The fix: agent preflight
The same validation pattern that works for human-driven UIs (validate before storing) works for agents — you just expose it as a tool the model can call before it acts.
For the OpenAI or Anthropic SDK route, the tool definition is a few lines:
const validateLocalDatetime = {
type: "function",
function: {
name: "validate_local_datetime",
description:
"Preflight check for a user-entered local datetime. Call BEFORE scheduling, booking, billing, or reminding from natural-language time references. Returns DST_GAP / DST_OVERLAP / INVALID_TIMEZONE so the agent knows when to ask the user to disambiguate.",
parameters: {
type: "object",
properties: {
local_datetime: { type: "string", description: "ISO 8601 local datetime, e.g. 2026-03-08T02:30:00" },
time_zone: { type: "string", description: "IANA timezone, e.g. America/New_York" },
},
required: ["local_datetime", "time_zone"],
},
},
};
Bind that to a fetch call to https://chronoshieldapi.com/v1/datetime/validate and the model has a deterministic preflight it can run on any user-entered time.
For Claude Desktop, Cursor, Windsurf, or any MCP-compatible client, there's an even shorter path — install the Model Context Protocol server and the model gets the tool natively, no integration code:
{
"mcpServers": {
"chronoshield": {
"command": "npx",
"args": ["-y", "chronoshield-mcp"],
"env": { "CHRONOSHIELD_API_KEY": "your_key" }
}
}
}
After restart, ask Claude: "Schedule a reminder for 2:30 AM on March 8, 2026 in New York." It calls the tool, sees DST_GAP, and asks you whether to use 3:00 AM instead — instead of silently confirming a moment that won't exist.
Free key at chronoshieldapi.com (no card). Full agent integration guide for OpenAI / Claude / LangChain / Vercel AI SDK at chronoshieldapi.com/docs/ai-agents.
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;
}
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)