Timezones, DST, and the IANA tz database: not getting trapped by time

4 min read

It’s tempting to treat timezones as “just add a UTC offset”, but in reality they involve DST transitions, historical offset changes, and political decisions that move them around. This article walks through the practical pitfalls and how to design around them.

A timezone is not just an offset

When people hear “timezone”, they often picture a fixed offset like UTC+9. The actual concept is:

  • A geographical region (e.g. Asia/Tokyo, America/New_York)
  • A history of offset changes
  • DST rules (or absence of them)

The same “New York” is UTC−4 in summer and UTC−5 in winter, and the exact transition dates have changed multiple times.

IANA tzdata: the canonical timezone database

The world’s timezone information lives in the IANA Time Zone Database (a.k.a. tzdata or zoneinfo):

  • Historical data: past DST transitions, offset changes, abandoned/restarted DST
  • ~600 zone definitions
  • Released several times a year as governments make new decisions

Examples of zone names:

  • Asia/Tokyo — JST, no DST, fixed UTC+9
  • America/New_York — EST/EDT, has DST
  • Europe/London — GMT/BST, has DST
  • UTC — offset 0, no DST
  • Etc/GMT+5 — UTC−5 (sign is inverted by POSIX convention)

Operating systems, Java, Python, and JavaScript’s Intl.DateTimeFormat all consult tzdata internally.

Why DST makes time handling hard

DST transitions create non-existent times and duplicate times.

Spring: time skips forward

In New York on the second Sunday of March:

02:00 EST → 03:00 EDT

02:30 EST does not exist. Parsing “2:30 AM” produces inconsistent behavior:

  • Some libraries normalize to “3:30 AM EDT”.
  • Others treat it as “2:30 AM EST” (using the pre-transition offset).
  • Some throw an error.

Fall: time repeats

In New York on the first Sunday of November:

02:00 EDT → 01:00 EST

01:30 happens twice. Given just “1:30 AM”, you can’t decide whether the UTC moment is 05:30 or 06:30.

Implementation impact

// "2024-03-10 02:30" in US Eastern, parsed in JavaScript
new Date('2024-03-10T02:30:00-05:00'); // EST → normalized to 03:30 EDT
new Date('2024-03-10T02:30:00-04:00'); // EDT explicitly

When only local time is given, the parser has to guess which offset to use. That’s the core of the problem.

Historical offset changes: why tzdata is non-negotiable

What was the time in Tokyo in 1948? Surprisingly, not simply UTC+9. Japan observed DST from 1948–1951. The Asia/Tokyo zone history includes:

1948-05-01 to 1948-09-11   JDT (UTC+10)
1948-09-12 to 1949-04-02   JST (UTC+9)
1949-04-03 to 1949-09-10   JDT (UTC+10)
...
1952-01-01 onward          JST (UTC+9) fixed

A fixed offset cannot represent this history. tzdata is required for accurate historical timestamps.

Political volatility: zones change without warning

Timezones change at the speed of legislation:

  • 2011: Samoa moved from UTC−11 to UTC+13 (jumped over the date line).
  • 2014: Parts of Russia abolished DST and moved to permanent standard time.
  • 2024: Multiple countries are debating DST abolition.

When such changes happen, tzdata releases an update, and OS/runtimes have to pick it up. Old systems running old tzdata miss future rules — calculating future timestamps with stale tzdata gives wrong answers.

API design: storing and transmitting time

Given the traps, the safe defaults are:

1. Store everything in UTC

Database timestamps should be UTC (or timestamptz). Apply timezone conversion only at the moment you display.

2. Represent display time as a UTC moment + zone name

To convey “2024-04-01 12:00 JST”:

{
	"timestamp": "2024-04-01T03:00:00Z",
	"timezone": "Asia/Tokyo"
}

A UTC moment with a zone name. A fixed offset +09:00 is fine for JST specifically, but ambiguous for zones with DST.

3. Use ISO 8601

2024-04-01T12:00:00+09:00 is:

  • Parseable in every language
  • Human-readable
  • Unambiguous as a moment

Locale-dependent strings like 2024/04/01 12:00:00 (JST) are bad API contracts.

4. Get the user’s zone from the browser

Intl.DateTimeFormat().resolvedOptions().timeZone returns an IANA zone name (e.g. Asia/Tokyo). Send that to the server and let the server convert when rendering.

The state of JavaScript date handling

JavaScript’s Date object has long been a weak point:

  • Only the local timezone and UTC are first-class.
  • Converting to arbitrary zones is awkward.
  • Mutable.

Where the ecosystem stands:

  • Moment.js — once standard, now in maintenance mode.
  • date-fns / dayjs — lightweight; zone handling lives in plugins.
  • Luxon — full zone API, well-suited to complex needs.
  • Temporal — proposed native API that will eventually replace much of this.

For new projects with non-trivial zone handling, Luxon today; date-fns + date-fns-tz is a solid alternative; await Temporal for native support.

Time-handling checklist

  • Store timestamps as UTC.
  • Pair display times with an IANA zone name.
  • Test code at DST transitions, especially the spring “skip”.
  • Have a way to update tzdata in your environment.
  • Default API formats to ISO 8601.
  • Detect the user’s zone via Intl.DateTimeFormat.

To convert a moment between zones (UTC, JST, PST, CET, …), the timezone converter on this site handles DST automatically and shows the result for the date you specify.