Skip to main content
DevBench
All articles
devtoolsjavascriptreference

Time Zones and DST for Developers: What Actually Goes Wrong

June 27, 20267 min read

Time zones cause more production bugs than almost any other single source of complexity. Not because they are mysterious, but because developers encounter three different things — Unix timestamps, UTC offsets, and named time zones — and treat them as interchangeable. They are not.

The three concepts

ConceptExampleWhat it represents
Unix timestamp1719446400Seconds since 1970-01-01 00:00:00 UTC. Absolute, unambiguous, time-zone-free.
UTC offset+05:30A fixed offset from UTC. Does NOT account for DST — it is just a number.
Named time zoneAmerica/New_YorkA political zone with a history of offsets and DST rules. The IANA database defines ~600 of them.

The critical mistake: storing +05:30 when you mean Asia/Kolkata. An offset is a snapshot; a named zone is a policy that changes over time.

What DST actually does

Daylight Saving Time (DST) shifts clocks forward by one hour in spring and back in autumn. The effect on a fixed UTC offset:

  • America/New_York is UTC−5 in winter (EST) and UTC−4 in summer (EDT)
  • Europe/London is UTC+0 in winter (GMT) and UTC+1 in summer (BST)
  • About 40% of the world does not observe DST — Asia/Tokyo and Asia/Kolkata are always UTC+9 and UTC+5:30 respectively

The two DST edge cases that break code

Gap — spring forward (2:00 AM → 3:00 AM)

When clocks spring forward, times between 2:00 and 3:00 AM do not exist. Code that constructs a DateTime at 2:30 AM on that day is creating an invalid time. Different libraries handle this differently — some throw, some silently shift forward, some shift backward. Know what yours does.

Fold — fall back (2:00 AM → 1:00 AM)

When clocks fall back, times between 1:00 and 2:00 AM occur twice. 2026-11-01T01:30:00 in America/New_York is ambiguous — it could be EDT (UTC−4) or EST (UTC−5). A correct representation must specify which fold: 2026-11-01T01:30:00-04:00 (first occurrence) or 2026-11-01T01:30:00-05:00 (second).

Rule: store UTC, display local

The universal rule for any application that stores timestamps:

  1. Store all timestamps as UTC (or Unix timestamp integers)
  2. Convert to local only at display time, using the user's time zone
  3. Never store a local time without its time zone information

A timestamp stored as 2026-06-15 14:00:00 with no time zone is a ticking bug. Is that UTC? The server's local time? The user's local time? Nobody knows.

JavaScript: Temporal API vs Date

JavaScript's built-in Date object is notoriously bad at time zones — it only knows UTC and the local system time zone. For anything more complex, use a library or the new Temporal API (Stage 3, available in Node 22+ with the --experimental-vm-modules flag and via polyfill):

// Temporal API — the correct modern approach
import { Temporal } from "@js-temporal/polyfill";

const utcNow = Temporal.Now.instant();

// Convert to New York time
const nyTime = utcNow.toZonedDateTimeISO("America/New_York");
console.log(nyTime.toString());
// 2026-06-15T10:00:00-04:00[America/New_York]

// Convert to Kolkata time
const koTime = utcNow.toZonedDateTimeISO("Asia/Kolkata");
console.log(koTime.toString());
// 2026-06-15T19:30:00+05:30[Asia/Kolkata]

// Safe arithmetic — skips DST gaps automatically
const tomorrow = nyTime.add({ days: 1 });

For production code today, use date-fns-tz or Luxon — both handle named time zones correctly using the IANA database.

PostgreSQL: timestamp vs timestamptz

PostgreSQL has two timestamp types:

  • timestamp (without time zone) — stores the value as-is with no zone awareness. Dangerous if your app and DB server are in different zones.
  • timestamptz (with time zone) — always stored as UTC internally, converted to the session time zone on display. Always use this.
-- Set session time zone for queries
SET TIME ZONE 'America/New_York';

-- Always use timestamptz for application timestamps
CREATE TABLE events (
  id         BIGSERIAL PRIMARY KEY,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Convert for display
SELECT created_at AT TIME ZONE 'Asia/Kolkata' AS kolkata_time
FROM events;

REST API: ISO 8601 with offset

Always include a time zone offset in API timestamps. 2026-06-15T14:00:00Z (the Z means UTC) is unambiguous. 2026-06-15T14:00:00 is not — clients may interpret it as local time, UTC, or refuse to parse it.

  • Use Z suffix for UTC: 2026-06-15T14:00:00Z
  • Use explicit offsets when storing user-local context: 2026-06-15T10:00:00-04:00
  • Never return bare local times from an API

Common bugs and fixes

BugFix
Recurring event fires at wrong time after DST changeSchedule in named time zone, not UTC offset
Age/duration calculation is off by 1 hourCompute in UTC; convert only for display
Dates jump when server time zone differs from userUse timestamptz in Postgres; store UTC everywhere
new Date("2026-06-15") returns previous day in USDate-only strings are parsed as UTC midnight; add time component or use a library

Try it yourself

Use the free browser-based Timezone Converter on DevBench — no signup, runs entirely in your browser.

Open Timezone Converter