Time Zones for Developers

10 min read

Time zone handling is one of the most reliably painful areas of software engineering. The bugs it produces tend to be subtle, intermittent, and embarrassing — your calendar app is fine for 363 days a year, then schedules a meeting an hour off for two days in March. This guide covers the most common mistakes and the patterns that prevent them.

Rule 1: Store UTC, display local

The most fundamental rule: always store timestamps in UTC. Never store a local time without its UTC offset.

If you store 2026-03-08 14:00:00 without a timezone, that string is ambiguous. It could be 2 PM in New York, 2 PM in London, or 2 PM in Tokyo — three completely different moments separated by up to 14 hours.

Store 2026-03-08T19:00:00Z(UTC) instead. Then convert to the user’s local timezone only at display time. This means:

  • Database columns should be TIMESTAMP WITH TIME ZONE (PostgreSQL) or equivalent
  • API responses should include the UTC offset: 2026-03-08T14:00:00-05:00
  • Client-side code converts UTC to local for display, never for storage

Rule 2: Use IANA timezone identifiers, not abbreviations

Timezone abbreviations are ambiguous. “CST” could mean:

  • US Central Standard Time (UTC−6)
  • China Standard Time (UTC+8)
  • Cuba Standard Time (UTC−5)
  • Australian Central Standard Time (UTC+9:30)

“IST” is Israel Standard Time (UTC+2), India Standard Time (UTC+5:30), or Irish Standard Time (UTC+1).

Always use IANA identifiers:America/Chicago,Asia/Shanghai,America/Havana,Australia/Adelaide. These are unambiguous, well-maintained, and supported by every major runtime and database.

Rule 3: Understand the DST gap and fold

DST creates two dangerous edge cases:

The gap (spring forward): When clocks spring forward, a one-hour block of local time simply does not exist. In New York, on March 8, 2026, the hour from 2:00 AM to 3:00 AM disappears — clocks jump from 1:59:59 AM directly to 3:00:00 AM. If you try to represent 2:30 AM Eastern time on that date, there is no valid local time to map it to. Code that converts UTC to local time handles this transparently. Code that tries to construct a local time directly and then convert to UTC may produce wrong results or throw.

The fold (fall back):When clocks fall back, a one-hour block of local time repeats. In New York, on November 1, 2026, the hour from 1:00 AM to 2:00 AM occurs twice — once in EDT (UTC−4) and once in EST (UTC−5). A timestamp of “1:30 AM Eastern” is ambiguous: it could be either 5:30 AM UTC or 6:30 AM UTC. If your application logs events by local time, repeated-hour events will look identical without an explicit UTC offset or UTC timestamp.

The safe approach: always work in UTC internally. Convert to local only for display, never for arithmetic or comparison.

Rule 4: Never hardcode UTC offsets

UTC offsets for a given location change at least twice a year for DST regions — and can change any time a government decides to reform its timezone. If you hardcode “New York is UTC−5,” your code will be wrong for the six months of the year when EDT (UTC−4) is in effect.

Use an IANA-backed library that reads from the timezone database:

  • JavaScript/TypeScript: Intl.DateTimeFormat (built-in), Luxon, date-fns-tz, Temporal (proposal)
  • Python: zoneinfo (stdlib, Python 3.9+), pytz (older)
  • Java: java.time.ZonedDateTime with ZoneId.of("America/New_York")
  • Go: time.LoadLocation("America/New_York")
  • PostgreSQL: AT TIME ZONE 'America/New_York'

Rule 5: Keep the timezone database updated

Governments change their timezone rules — sometimes with very little notice. Russia eliminated DST across its entire territory in 2014. Turkey permanently shifted to UTC+3 in 2016. Morocco changes its DST rules almost every year around Ramadan.

The IANA timezone database is updated multiple times per year to reflect these changes. Operating system updates usually include timezone database updates, which is why keeping servers and containers patched matters even when you think it’s “just a security update.” Applications that bundle their own timezone data (some Java apps, some containers with frozen base images) need to update that data independently.

Rule 6: Be careful with JavaScript’s Date object

JavaScript’s native Date object has several gotchas:

  • new Date('2026-03-08') parses as midnight UTC, while new Date('2026-03-08T00:00:00') parses as midnight local time. This inconsistency surprises many developers.
  • Date.getMonth() returns 0–11 (zero-indexed), not 1–12. Off-by-one errors are common.
  • toLocaleDateString()and related methods use the browser’s locale and timezone, which may differ from the user’s actual preference if they’ve moved or are using a borrowed device.

For anything beyond simple display, use Luxon or the emerging TemporalAPI (which explicitly distinguishes between “wall clock time” and “instant in time”).

Testing time zone code

Time zone bugs are often invisible until a specific date or a specific transition. Good tests:

  • Test around DST transition dates, not just arbitrary dates
  • Test the gap hour (e.g., 2:30 AM on spring-forward Sunday)
  • Test the fold hour (e.g., 1:30 AM on fall-back Sunday) with both possible UTC interpretations
  • Test with a timezone that does not observe DST (e.g., Asia/Kolkata)
  • Test with a half-hour offset timezone (e.g., Asia/Kolkata at UTC+5:30)
  • Test around midnight UTC when two calendar dates might be simultaneously current in different timezones
  • Test with Pacific/Kiritimati (UTC+14, the world’s most advanced timezone)

In JavaScript, you can temporarily override the system timezone with the TZ environment variable: TZ='America/New_York' node test.js. Vitest and Jest can be configured to run tests in specific timezones.