Chapter 07·The Intl toolbox·~8 min
Time zones, DST, and Lord Howe Island
There are 38 distinct UTC offsets in use today, including half-hour and 45-minute zones. Samoa skipped December 30, 2011. Lord Howe Island shifts the clock 30 minutes for DST. Storing user timestamps as 'UTC plus a number' is how features break twice a year.
The problem
Time zones aren't a math problem. They're a political record, maintained in the IANA time zone database and updated several times most years. Russia abolishes DST, then reinstates parts of it. Turkey moves to permanent UTC+3. Morocco pauses DST for Ramadan. Code that stores "UTC + 5 hours" instead of "Asia/Karachi" breaks the day the rule changes, and calendar invites and billing cycles start firing at the wrong time.
How it works
An offset is a moment; a zone is a history
UTC+2 says what a clock reads right now. Europe/Berlin says what clocks there have read for over a century and what they will do next spring. A zone name is a key into the IANA time zone database, a curated archive of political decisions: countries adopt DST, abolish it, nudge offsets, and once in a while one hops the date line and skips a calendar day.
This is why the storage rule has two halves. For things that happened, store the instant (UTC). For things scheduled by humans ("March 3rd, 9 AM, my time"), store wall time plus the zone name. The promise is about the local clock, and governments change the mapping between clocks and instants with months of notice. tzdata ships multiple releases most years to keep up, and an offset frozen at booking time cannot.
Never let arithmetic assume whole hours: India runs +5:30, Nepal +5:45, and Lord Howe Island's DST shifts the clock thirty minutes, not sixty. Rounding those offsets to the whole hour moves real appointments.
The demo
Try these, in order
Each step reproduces one specific failure in the demo below.
- 1Keep Australia/Lord_Howe selected and read “the flip, frame by frame”. 01:59 at +10:30 becomes 02:30 at +11:00: a thirty-minute DST jump. “DST is always one hour” is a falsehood with a named counterexample.
- 2Pick “Pacific/Apia · skipped a calendar day”. 23:59 on Dec 29, 2011 rolls directly into Dec 31. The frame-by-frame shows a whole calendar date that never existed locally.
- 3Find Asia/Kathmandu and Asia/Kolkata in the live world clock. +05:45 and +05:30: the minutes column differs from UTC's. A UI that renders only “GMT+5” is off by 45 minutes for Nepal.
- 4Pick “Africa/Casablanca · pauses for Ramadan”. Morocco sits on UTC+1 all year and steps back to UTC+0 for Ramadan, which moves through the calendar. The two changes land on different dates every year, and only tzdata keeps up.
- 5In “Convert a meeting”, keep the defaults (09:00 on 2026-10-28, “Source zone” Europe/London) and compare the two target rows. New York reads 05:00 at GMT-4 and carries the badge “DST boundary within a week of this meeting”. US clocks fall back on Nov 1, 2026, so the same wall time one week later is a different instant. Tokyo reads 18:00 at GMT+9 with no badge: Japan has no DST.
- 6Change “Source zone” to America/New_York and set “Meeting time” to 02:30 on 2026-03-08. A warning appears: that wall time does not exist in New York. Clocks jump from 02:00 to 03:00 at the spring DST boundary. Every scheduler needs a policy for times its users can type but their clocks will never show.
Standard offset is UTC+10:30. DST adds 30 minutes, not 60. The only inhabited place on Earth with a half-hour DST flip.
The flip, frame by frame
01:59 just before DST
4 Oct 2026, 01:59
UTC+10:30
02:30 just after DST
4 Oct 2026, 02:30
UTC+11:00
At 02:00 the clock jumped straight to 02:30.
Convert a meeting
Type a wall-clock time, say whose wall it is, and read the same instant in up to three other zones. Each row is Intl.DateTimeFormat with timeZone + timeZoneName.
| Zone | Meeting reads | Offset | |
|---|---|---|---|
| Europe/London (source) | Wed, 28 Oct 2026, 09:00 GMT | UTC+00:00 | |
| America/New_York | Wed, 28 Oct 2026, 05:00 GMT-4 | UTC-04:00 | DST boundary within a week of this meeting |
| Asia/Tokyo | Wed, 28 Oct 2026, 18:00 GMT+9 | UTC+09:00 |
The warning compares each target zone's UTC offset at the meeting with its offset 7 days later. When they differ, a recurring “same wall time next week” event lands at a different instant, the classic cross-zone standup bug.
Live world clock · —
The same instant, formatted in every featured zone. Refresh rate: 1 Hz. The offset column uses timeZoneName: 'longOffset'.
| IANA zone | Local time | Offset |
|---|---|---|
| UTC | — | — |
| Pacific/Honolulu | — | — |
| America/Los_Angeles | — | — |
| America/New_York | — | — |
| America/Argentina/Buenos_Aires | — | — |
| Europe/London | — | — |
| Europe/Berlin | — | — |
| Africa/Cairo | — | — |
| Asia/Kolkata | — | — |
| Asia/Kathmandu | — | — |
| Asia/Tokyo | — | — |
| Australia/Lord_Howe | — | — |
| Pacific/Auckland | — | — |
| Pacific/Apia | — | — |
Things almost everyone believes that are wrong
- Time zones have whole-hour offsets. False. India is +05:30, Nepal is +05:45, Lord Howe is +10:30 standard and +11:00 in DST.
- DST always shifts the clock by an hour. False: see Lord Howe (30 min).
- Every day has 24 hours. False. The day DST starts in spring has 23. The day DST ends in autumn has 25. Samoa's Dec 30, 2011 had zero hours.
- UTC and GMT are the same thing. Close enough for most apps. But different bodies define them, and they can drift by up to 0.9 seconds before a leap-second correction.
- A country has one timezone. China spans five geographically but enforces one (UTC+8). The US has six. France has twelve (counting overseas territories, the most of any country).
Engineering rules
- Store instants in UTC ISO-8601 with milliseconds.
- Format on the client with
Intl.DateTimeFormat(locale, { timeZone: userTz }). - Take the user's tz from
Intl.DateTimeFormat().resolvedOptions().timeZone. Do not ask the user to pick from a dropdown unless they are a sysadmin. - Round-trip test fixtures through a DST flip (March + October). Bugs hide in the spring-forward boundary.
- Update the IANA tzdata at least quarterly. Governments change tz rules on short notice, and a pinned container image will not pick up the patch on its own.
The short version
Store UTC instants plus an IANA zone name for anything human-scheduled. Offsets are political, they change, and some of them end in :30 and :45.
What to do about it
- Always store IANA time-zone IDs (
Europe/Berlin,America/Sao_Paulo), not offsets. Offsets describe a moment; IDs describe a region. - For display, use
new Intl.DateTimeFormat(locale, { timeZone: id, timeZoneName: "long" }). The browser handles DST transitions, abbreviations, and historical offset changes. - For recurring events ("every Tuesday at 10am"), store the rule in the user's local time-zone ID and resolve to a UTC instant per occurrence. Storing as a UTC instant breaks across DST changes.
- Subscribe to the IANA tzdata announcement list and bump the tzdata package in the runtime within a week of each release.
Where this comes up
Who it concerns
Moments
- ·Designing scheduling, calendar, or recurring billing features
- ·DST changeover (twice a year)
- ·Launching into a region with a non-standard offset (India, Iran, Nepal, Australia)
Field note
On 29 December 2011, Samoa ended the day and went straight to 31 December. The country hopped the International Date Line to align its week with Australia and New Zealand, and 30 December 2011 never happened there. tzdata records the skipped day, and offset arithmetic cannot, so any system that stored “next appointment: Dec 30, local” had an appointment on a date that did not exist.