DEV Community

kol kol
kol kol

Posted on

My API Broke Every January 1st — The Timezone Bug I Should Have Caught in Code Review

My API broke at exactly 00:00 UTC on January 1st. Not the users' midnight — UTC midnight. Which meant our users in Tokyo had been living with broken data since 9 AM their time.

And the worst part? The tests all passed. The staging environment worked fine. It only broke in production, because production is in a different timezone than staging.

The Bug

Here's what the code looked like:

function getDailyReport(date) {
  const start = new Date(date).toISOString().split('T')[0];
  const end = new Date(start + 'T23:59:59Z');

  return db.reports.findMany({
    where: {
      createdAt: { gte: new Date(start), lt: end }
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Seems fine, right? toISOString() gives you UTC. We're filtering by date. What could go wrong?

Here's what went wrong: new Date(date) when date is just "2026-01-01" (no time component) gets interpreted in the local timezone. In staging (UTC server), "2026-01-01"2026-01-01T00:00:00.000Z. In production (US-East server), "2026-01-01"2026-01-01T05:00:00.000Z.

Five hour offset. Every single date query. For an entire year before anyone noticed.

Why Tests Passed

Our CI runs in Docker containers set to UTC. Our staging server is also UTC. Our production server? US-East. The timezone mismatch was invisible until New Year's Day rolled around and the date boundary crossed the timezone offset.

Staging (UTC):     2026-01-01 → Jan 1 00:00 UTC ✅
Production (EST):  2026-01-01 → Jan 1 05:00 UTC ❌
Enter fullscreen mode Exit fullscreen mode

We lost 5 hours of data on every query. The reports showed numbers that were "close enough" that nobody flagged it for 12 months.

The Fix

function getDailyReport(date: string) {
  // Always append time to force UTC interpretation
  const start = new Date(`${date}T00:00:00Z`);
  const end = new Date(`${date}T23:59:59.999Z`);

  return db.reports.findMany({
    where: {
      createdAt: { gte: start, lt: end }
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

One line change. Append T00:00:00Z to force the Date constructor into UTC mode. No more ambiguity.

The Real Fix (Process, Not Code)

The code fix took 30 seconds. The real fix took a week:

  1. Added a timezone assertion in CI — our test suite now explicitly checks that process.env.TZ === 'UTC'. If anyone changes the CI timezone, tests fail.

  2. Set TZ=UTC in all Dockerfiles — every container, every environment, same timezone. No surprises.

  3. Added a timezone check to our deploy scriptdate +%Z must return UTC before deploy proceeds.

  4. Wrote a linter rule — flags any new Date(string) where the string doesn't contain timezone info.

The Lesson

Timezone bugs are sneaky because they don't crash. They produce wrong data that looks right. Your users won't get an error page — they'll get silently incorrect numbers, and they'll trust them.

Three rules I now follow:

  • Never trust the system timezone. Always set TZ=UTC explicitly.
  • Never parse dates without timezones. "2026-01-01" is ambiguous. "2026-01-01T00:00:00Z" is not.
  • Never assume your CI timezone matches production. Assert it in your tests.

I've been coding for years. I still got bit by this. If it can happen to me, it can happen to you.

Top comments (0)