Timezone Handling for Developers: UTC, DST, and Common Bugs
Timezone bugs are among the hardest bugs to reproduce and debug. They appear only at certain times of year, differ across regions, and often pass undetected until a real user in a real timezone reports an issue. This guide explains how timezones work and how to handle them correctly.
Convert timestamps across timezones instantly with our Unix Timestamp Converter.
The Core Rule: Store in UTC, Display Locally
This single rule prevents the majority of timezone bugs:
- Store all timestamps in UTC in your database
- Convert to the user’s local timezone only when displaying
// Store: always UTC
const now = new Date(); // Internally UTC in JavaScript
db.save({ createdAt: now.toISOString() }); // "2026-03-15T14:30:00.000Z"
// Display: convert to user's timezone
const userTimezone = 'America/New_York';
const formatted = new Intl.DateTimeFormat('en-US', {
timeZone: userTimezone,
dateStyle: 'long',
timeStyle: 'short',
}).format(new Date(storedTimestamp));
// "March 15, 2026 at 10:30 AM"
UTC Is Not a Timezone
UTC (Coordinated Universal Time) is the primary time standard — it has no DST adjustments and never changes. It is the reference from which all other timezones are measured.
| Term | Meaning |
|---|---|
| UTC | The reference time standard — never changes |
| GMT | Greenwich Mean Time — historically identical to UTC for most purposes |
| UTC+0 | The UTC offset, not a timezone (some use it interchangeably with UTC) |
| Z (Zulu) | Military notation for UTC in ISO 8601 strings |
2026-03-15T14:30:00Z and 2026-03-15T14:30:00+00:00 both mean 14:30 UTC.
How Timezones Work
A timezone is more than an offset. It is a named zone with a history of offset changes due to Daylight Saving Time, government decisions, and historical adjustments. The IANA timezone database (the tz database) tracks all of this:
America/New_York → UTC-5 in winter (EST), UTC-4 in summer (EDT)
Europe/London → UTC+0 in winter (GMT), UTC+1 in summer (BST)
Asia/Kolkata → UTC+5:30 (no DST — fixed offset)
Always use IANA timezone names (like America/New_York) rather than abbreviations (like EST). Abbreviations are ambiguous — CST is UTC-6 in the US, UTC+8 in China, and UTC+9:30 in Australia.
Daylight Saving Time (DST)
DST is the practice of advancing clocks by one hour during summer months to extend evening daylight.
DST Transition Bugs
Spring forward: Clocks jump from 2:00 AM to 3:00 AM. The hour from 2:00 to 2:59 AM does not exist on this day. Code that creates a date at 2:30 AM local time will receive unexpected behavior.
Fall back: Clocks return from 3:00 AM to 2:00 AM. The hour from 2:00 to 2:59 AM occurs twice. Events in this window are ambiguous without an explicit offset.
// Bug: this date doesn't exist in America/New_York on 2026-03-08 (spring forward)
const invalid = new Date('2026-03-08T02:30:00'); // Implicitly local time — undefined behavior
// Fix: always work in UTC
const safe = new Date('2026-03-08T07:30:00Z'); // UTC — unambiguous
Countries Without DST
Not all countries observe DST. If your application serves global users, never assume DST applies:
- No DST: India, China, Japan, most of Africa and South/Southeast Asia
- With DST: United States, Canada, European Union, Australia, parts of South America
ISO 8601 — The Standard Format
ISO 8601 is the international standard for date/time representation. Always use it for data exchange:
| Format | Example | Notes |
|---|---|---|
| Date | 2026-03-15 | Year-Month-Day |
| DateTime (UTC) | 2026-03-15T14:30:00Z | Z = UTC |
| DateTime (with offset) | 2026-03-15T09:30:00-05:00 | Explicit offset |
| DateTime (local, no offset) | 2026-03-15T14:30:00 | Ambiguous — avoid in APIs |
| Week date | 2026-W11-7 | Year-Week-DayOfWeek |
The Z suffix is critical. 2026-03-15T14:30:00 without a timezone is interpreted as local time, which varies by server and client. Always include Z or an explicit offset in API responses.
JavaScript Timezone Pitfalls
Date constructor is local time
// Local time — timezone-dependent!
new Date('2026-03-15') // Parsed as UTC midnight (date-only strings)
new Date('2026-03-15T00:00:00') // Parsed as LOCAL midnight — varies by browser/server
// Safe — always UTC
new Date('2026-03-15T00:00:00Z')
new Date(Date.UTC(2026, 2, 15)) // Month is 0-indexed!
Date.getMonth() is 0-indexed
const d = new Date('2026-03-15T00:00:00Z');
d.getMonth(); // 2 (not 3!)
d.getUTCMonth(); // 2
// Always use UTC methods when working with UTC dates
d.getUTCFullYear(); // 2026
d.getUTCDate(); // 15
d.getUTCHours(); // 0
Use Intl.DateTimeFormat for localized display
const date = new Date('2026-03-15T14:30:00Z');
// Format for Tokyo
new Intl.DateTimeFormat('ja-JP', {
timeZone: 'Asia/Tokyo',
dateStyle: 'full',
timeStyle: 'long',
}).format(date);
// "2026年3月15日日曜日 23時30分00秒 日本標準時"
// Format for New York
new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
dateStyle: 'medium',
timeStyle: 'short',
}).format(date);
// "Mar 15, 2026, 10:30 AM"
Database Best Practices
PostgreSQL
- Use
TIMESTAMPTZ(timestamp with time zone) — stores UTC internally, converts to session timezone on read - Avoid
TIMESTAMP(without time zone) — stores local time without offset, leading to ambiguity
-- Good: stores UTC, returns in session timezone
CREATE TABLE events (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Always query in UTC
SET TIME ZONE 'UTC';
SELECT created_at FROM events;
MySQL / MariaDB
- Use
DATETIMEwith explicit UTC handling, orTIMESTAMP(auto-converts to UTC) TIMESTAMPcolumns have a range limit of 2038-01-19 (Year 2038 problem)DATETIMEstores exactly what you put in — no timezone conversion
-- Insert UTC explicitly
INSERT INTO events (created_at) VALUES (UTC_TIMESTAMP());
-- Convert to user timezone on read
SELECT CONVERT_TZ(created_at, 'UTC', 'America/New_York') AS local_time FROM events;
Server and Application Configuration
Always run servers in UTC
# Set system timezone on Linux
sudo timedatectl set-timezone UTC
# Verify
date
# Mon Mar 15 14:30:00 UTC 2026
In Node.js, the timezone of new Date() follows the system timezone. If your server is not in UTC, results will vary between environments.
# Force UTC for a Node.js process
TZ=UTC node server.js
Checklist: Timezone-Safe Code
- All timestamps stored in UTC in the database
- API responses include timezone offset (
Zor+00:00) - User-facing dates converted to user’s IANA timezone on display
- No bare
TIMESTAMPcolumns in MySQL without UTC enforcement - Servers configured to run in UTC
- No hardcoded UTC offsets (use IANA names instead)
- DST transitions tested for scheduled jobs near 2:00 AM
- Date-only strings handled carefully (
YYYY-MM-DDwithout time defaults to midnight UTC in some parsers, midnight local in others)