How to Store and Handle Time Zones in Software
Time zone bugs are among the most reliably annoying in software. They don’t show up in tests. They appear in production, twice a year, when clocks change — and by then it’s someone’s missed meeting.
The root cause is almost always the same: a UTC offset stored where an IANA zone name should be.
Never store raw UTC offsets
An offset like −5 or +05:30 captures the current difference from UTC. It says nothing about what that offset will be in March, or what happens when a government decides to stop observing DST. Storing UTC−5 to represent “New York time” is correct for half the year and wrong for the other half.
Don’t do this:
meeting_time: "2026-03-10T14:00:00-05:00"
user_timezone: "UTC-5"
Store IANA time zone names
The IANA Time Zone Database (America/New_York, Europe/London, Asia/Kolkata) encodes the full rule set for a region — every past and future daylight saving transition. All major languages and runtimes ship with it.
Do this instead:
meeting_time_utc: "2026-03-10T19:00:00Z" // moment in time, always UTC
user_timezone: "America/New_York" // rule set for display
Store the moment in UTC. Store the user’s IANA name separately. Derive the local display time at render time, not at storage time.
Use the platform’s tz-aware APIs
Most runtimes give you a time zone–aware API that accepts IANA names. Prefer these over manual offset arithmetic.
JavaScript / TypeScript:
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
dateStyle: "full",
timeStyle: "short",
});
formatter.format(new Date("2026-03-10T19:00:00Z"));
// → "Tuesday, March 10, 2026 at 3:00 PM"
Python:
from zoneinfo import ZoneInfo # Python 3.9+
from datetime import datetime
utc_dt = datetime(2026, 3, 10, 19, 0, tzinfo=ZoneInfo("UTC"))
ny_dt = utc_dt.astimezone(ZoneInfo("America/New_York"))
# → 2026-03-10 15:00:00-04:00 (EDT, not EST)
PostgreSQL:
-- Store as timestamptz (UTC internally), display with AT TIME ZONE
SELECT meeting_at AT TIME ZONE 'America/New_York' FROM meetings;
Validate IANA names from user input
If users can enter or select their time zone, validate the value against the IANA database before saving. Invalid or legacy names (US/Eastern instead of America/New_York) can behave inconsistently across platforms.
JavaScript:
function isValidIANA(tz: string): boolean {
try {
Intl.DateTimeFormat(undefined, { timeZone: tz });
return true;
} catch {
return false;
}
}
Keep the IANA database updated
The IANA database is updated several times a year when governments change their DST rules. Operating systems and runtimes ship these updates — keep them current, especially for server environments that don’t update automatically. Outdated IANA data is a less common source of time zone bugs, but it does happen, particularly in long-running server processes.
For Node.js projects, the @js-joda/timezone or luxon packages bundle their own IANA data independently of the OS, which gives you more control over when you pick up updates.
The short version: store IANA names, not offsets. Store moments in UTC. Convert to local time at display time. Everything else follows from that.