NC Logo UseToolSuite
Time & Date

Timezone Handling for Developers: UTC, DST, and Common Bugs

Learn how to handle timezones correctly in web applications. Covers UTC vs. local time, Daylight Saving Time pitfalls, ISO 8601, and best practices for JavaScript, Python, and databases.

Necmeddin Cunedioglu Necmeddin Cunedioglu

Practice what you learn

Unix Timestamp Converter

Try it free →

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.

TermMeaning
UTCThe reference time standard — never changes
GMTGreenwich Mean Time — historically identical to UTC for most purposes
UTC+0The 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:

FormatExampleNotes
Date2026-03-15Year-Month-Day
DateTime (UTC)2026-03-15T14:30:00ZZ = UTC
DateTime (with offset)2026-03-15T09:30:00-05:00Explicit offset
DateTime (local, no offset)2026-03-15T14:30:00Ambiguous — avoid in APIs
Week date2026-W11-7Year-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 DATETIME with explicit UTC handling, or TIMESTAMP (auto-converts to UTC)
  • TIMESTAMP columns have a range limit of 2038-01-19 (Year 2038 problem)
  • DATETIME stores 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 (Z or +00:00)
  • User-facing dates converted to user’s IANA timezone on display
  • No bare TIMESTAMP columns 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-DD without time defaults to midnight UTC in some parsers, midnight local in others)
Necmeddin Cunedioglu
Necmeddin Cunedioglu Author

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.