Handling Timezones in Web Applications — A Practical Guide

Timezone bugs are among the most embarrassing bugs in software: a meeting reminder that fires an hour late, an invoice dated yesterday, a "daily" report that skips a day for users in Sydney. They pass every test on your machine and explode in production because your machine and your users do not share a clock. The good news is that timezone handling follows a small set of rules that, once adopted, eliminate almost all of these bugs. This guide covers the storage rule, IANA names versus offsets, the JavaScript Intl API, server-side rendering pitfalls, cross-zone scheduling, and how to actually test all of it.

The Golden Rule: Store UTC, Convert at Display

Every timestamp in your database, message queue, log file, and API payload should be in UTC — either an ISO 8601 string with a Z suffix or a Unix epoch number. Conversion to a human-readable local time happens exactly once, at the last possible moment: when the value is rendered for a specific user.

Why UTC? It is unambiguous (no "which 1:30 AM?" during a daylight saving fallback), it sorts and compares correctly as plain text or numbers, and it never shifts. Local times fail all three tests. If you store "2026-11-01 01:30" from a US user, you literally cannot know which instant it refers to — that wall-clock time occurs twice on the DST fallback night.

-- Good: unambiguous, sortable, DST-proof
created_at TIMESTAMPTZ  -- Postgres stores UTC internally
'2026-07-15T09:30:00Z'  -- ISO 8601 with Z = UTC
1784107800              -- Unix epoch seconds

-- Bad: ambiguous, breaks on DST, unsortable across users
'15/07/2026 3:00 PM'    -- whose 3 PM?

IANA Names vs Fixed Offsets

When you need to remember a user's timezone, store the IANA identifier (Asia/Kolkata, America/New_York, Europe/London), never a raw offset like +05:30 or -05:00. The difference matters because of daylight saving time.

  • Asia/Kolkata — always UTC+5:30. India abolished seasonal clock changes long ago, so IST never moves. Lucky us.
  • America/New_York — UTC-5:00 in winter, UTC-4:00 in summer. A stored "-05:00" is wrong for half the year.
  • Europe/London — UTC+0:00 in winter, UTC+1:00 in summer. "GMT" and "London time" are not the same thing year-round.
  • Australia/Sydney — DST in the southern hemisphere runs October to April, the opposite season from the north.

An IANA name carries the full rule set — past and future transitions — via the tz database that ships with your OS, browser, and language runtimes. An offset is a snapshot that silently becomes wrong. Get the user's zone in the browser with Intl.DateTimeFormat().resolvedOptions().timeZone and persist that string with their profile.

Converting for Display with the JavaScript Intl API

Modern JavaScript needs no library for display conversion. Intl.DateTimeFormat accepts a timeZone option and handles DST, locale conventions, and 12/24-hour preferences:

const ts = new Date('2026-07-15T09:30:00Z'); // stored UTC

// Detect the viewer's zone
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// e.g. "Asia/Kolkata"

// Render in the viewer's zone and locale
new Intl.DateTimeFormat('en-IN', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: zone,
}).format(ts);
// "15 Jul 2026, 3:00 pm"

// Render the SAME instant for a New York colleague
new Intl.DateTimeFormat('en-US', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: 'America/New_York',
}).format(ts);
// "Jul 15, 2026, 5:30 AM"

Note that the underlying Date object never changes — it represents one instant. Only the formatting differs. If you need parsing, arithmetic, or "next Tuesday in this zone" logic, reach for a library such as date-fns-tz, Luxon, or the emerging Temporal API, but keep the storage format UTC regardless.

SSR vs Client: The Hydration Trap

In Next.js and other SSR frameworks, the server renders HTML first and the browser hydrates it. If your component formats a date with the runtime's default zone, the server (usually running in UTC) produces one string and the client (running in the user's zone) produces another — a hydration mismatch warning at best, a flash of wrong time at worst.

  • Option 1: render a stable UTC or ISO string on the server, then re-format in useEffect after mount, when the real browser zone is known.
  • Option 2: store the user's IANA zone server-side (in their profile or a cookie) and pass it explicitly to Intl on both server and client so both render identical output.
  • Option 3: use suppressHydrationWarning on the specific element as a last resort, accepting one repaint.

The same trap exists in APIs: never let a backend format dates using its own server zone. Cloud servers commonly run in UTC, but a lift-and-shift VM configured for IST will happily bake +5:30 into every response until someone notices.

Scheduling Events Across Timezones

Scheduling is the one case where "store UTC" needs a footnote. A one-off event ("deploy at 2026-08-01 02:00 UTC") stores perfectly as UTC. But a recurring human-anchored event — "standup at 9:30 AM every weekday" — must store the wall-clock time plus the IANA zone, because the correct UTC instant changes whenever that zone enters or leaves DST.

// Recurring event: store intent, compute instants
{
  "rule": "every weekday",
  "localTime": "09:30",
  "timeZone": "America/Chicago"
}
// Winter occurrence -> 15:30 UTC
// Summer occurrence -> 14:30 UTC (DST active)

Compute the concrete UTC instant for each occurrence at scheduling time (or on the fly) using the tz database. This is also why cron jobs pinned to server time drift for international users — see our cron timezone guide for the server-side version of this problem.

Testing Timezone Logic

  • Run tests in a hostile zone. Set TZ=Pacific/Kiritimati (UTC+14) or TZ=America/New_York in CI, not UTC. Code that only works in UTC is hiding bugs.
  • Test the DST boundaries. Use fixed instants just before and after a spring-forward and a fall-back transition, plus the ambiguous repeated hour.
  • Test half-hour and 45-minute zones. Asia/Kolkata (+5:30) and Asia/Kathmandu (+5:45) catch code that assumes whole-hour offsets.
  • Freeze the clock. Use fake timers (Jest fake timers, freezegun in Python) so tests are deterministic.
  • Verify a real timestamp end to end. Paste an epoch value into a timestamp converter and confirm your UI shows the same instant in the target zone.

Frequently Asked Questions

Should I store dates in UTC or local time?

Store timestamps in UTC (or as Unix epoch values) and convert to the user's local timezone only at display time. UTC is unambiguous, sorts correctly, and is unaffected by daylight saving time changes.

What is the difference between an IANA timezone name and a UTC offset?

An IANA name like Asia/Kolkata identifies a region and all its historical and future rules, including daylight saving transitions. A fixed offset like +05:30 is only a snapshot and becomes wrong twice a year in zones that observe DST. Always persist IANA names, not offsets.

Does India (IST) have daylight saving time?

No. Indian Standard Time is fixed at UTC+5:30 all year, so Asia/Kolkata never shifts. However, apps serving users in the US, Europe, or Australia must still handle DST, because those zones change their offsets twice a year.

Try the Free Timestamp Converter

Convert Unix timestamps to human-readable dates in any timezone — and back — right in your browser. No signup, no cost.

Related articles