← Back to the tool

Time Zones in Programming — UTC, IANA, DST and Common Bugs

Updated: May 2026

Timezone bugs are among the most persistent and embarrassing in software: a scheduling app that books meetings an hour off twice a year, a log file that shows timestamps jumping backward at 2 AM, or a database query that silently returns rows from the wrong day. Almost all of these stem from the same root cause: mixing up a fixed UTC offset with a time zone that observes DST. This page covers the correct mental model and practical patterns in JavaScript, Python, and SQL.

Test your time zone conversions →

Free · IANA-based · No upload · In your browser

The three rules of timezone-safe code

  • Store UTC everywhere: All timestamps in databases, logs, APIs, and queues must be stored in UTC. Never store a local time without its offset or zone name.
  • Use IANA zone names, not abbreviations: "America/New_York" is unambiguous. "EST" is ambiguous — it refers to UTC-5 in winter and some people (wrongly) use it year-round for UTC-4. IST could mean India Standard Time, Irish Standard Time, or Israel Standard Time. Abbreviations are human-readable shorthands, not programming primitives.
  • Display in the user's local zone: Convert from UTC to IANA zone at the presentation layer only. Never convert in the database or during data processing.

A timestamp in UTC paired with a user's stored IANA zone name gives you everything you need to display the correct local time, handle DST transitions, and compare events globally — without any ambiguity. This is the only pattern that works correctly year-round.

JavaScript: the Intl API and avoiding Date pitfalls

JavaScript's Date object internally stores time as milliseconds since the Unix epoch (UTC). The common pitfall is calling new Date('2026-03-08 09:00:00'), which most browsers interpret as local time, not UTC, producing inconsistent results across machines and time zones.

JavaScript
// WRONG: parsed as local time, result depends on the server's timezone
const d = new Date('2026-03-08 09:00:00');

// CORRECT: ISO 8601 with explicit Z suffix forces UTC parsing
const d = new Date('2026-03-08T09:00:00Z');

// Display in a specific IANA zone
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'full',
  timeStyle: 'long',
});
console.log(formatter.format(d)); // "Sunday, March 8, 2026 at 4:00:00 AM EST"

// Get the offset in minutes for America/New_York on a specific date
function getOffsetMinutes(tz, date) {
  const utcMs = date.getTime();
  const localStr = new Intl.DateTimeFormat('en-CA', {
    timeZone: tz,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    hour12: false,
  }).format(date);
  // parse localStr and compute localMs, then return (localMs - utcMs) / 60000
}

The Intl API uses the browser's or Node.js's IANA time zone database. It handles DST correctly because it looks up the full rule set for the zone, not just a fixed offset. Avoid any library or pattern that maps zone abbreviations to fixed offsets — those will silently break twice a year.

Python: aware vs naive datetimes

Python distinguishes between "naive" datetimes (no timezone attached) and "aware" datetimes (timezone attached). The critical rule: never mix naive and aware datetimes. All code that crosses a system boundary (file, database, API) must use aware datetimes in UTC.

Python
from datetime import datetime, timezone
import zoneinfo  # Python 3.9+

# WRONG: naive datetime — no timezone information
naive = datetime(2026, 3, 8, 9, 0, 0)

# CORRECT: aware UTC datetime
utc_now = datetime.now(tz=timezone.utc)

# Convert UTC to a named IANA zone (handles DST automatically)
ny_zone = zoneinfo.ZoneInfo('America/New_York')
ny_time = utc_now.astimezone(ny_zone)
print(ny_time.strftime('%Y-%m-%d %H:%M %Z'))  # includes EDT or EST

# Parse an ISO 8601 string with offset
from datetime import datetime
dt = datetime.fromisoformat('2026-03-08T14:00:00+00:00')
# Attach IANA zone for DST-aware conversion
dt_with_zone = dt.replace(tzinfo=timezone.utc).astimezone(
    zoneinfo.ZoneInfo('Europe/London')
)

The zoneinfo module (Python 3.9+) reads the system IANA database. For older Python, the pytz or tzdata packages provide the same database. Avoid hard-coded UTC offsets like timezone(timedelta(hours=5, minutes=30)) — they won't update when DST rules change.

SQL: TIMESTAMP WITH TIME ZONE

Most databases offer two timestamp types: TIMESTAMP (stores exactly what you give it, no timezone) and TIMESTAMP WITH TIME ZONE (PostgreSQL) or DATETIME with timezone handling (MySQL, SQL Server). The rules are the same: always use the timezone-aware type and always insert UTC.

SQL (PostgreSQL)
-- Column definition: always use TIMESTAMPTZ (= TIMESTAMP WITH TIME ZONE)
CREATE TABLE events (
    id BIGSERIAL PRIMARY KEY,
    title TEXT,
    starts_at TIMESTAMPTZ NOT NULL,
    user_timezone TEXT NOT NULL  -- store IANA zone name separately
);

-- Insert: always pass UTC
INSERT INTO events (title, starts_at, user_timezone)
VALUES ('Team call', '2026-03-08T14:00:00Z', 'America/New_York');

-- Query: PostgreSQL converts to any zone on the fly
SELECT title,
    starts_at AT TIME ZONE 'America/New_York' AS local_time
FROM events;
-- Returns: "2026-03-08 09:00:00" (4 AM UTC → 9 AM EST, before the DST switch)

-- Comparison across zones is always correct with TIMESTAMPTZ
SELECT * FROM events
WHERE starts_at BETWEEN '2026-03-01'::TIMESTAMPTZ AND '2026-04-01'::TIMESTAMPTZ;

Never store "2026-03-08 09:00:00" as a plain TIMESTAMP in PostgreSQL with the intent of it being New York time. A plain TIMESTAMP has no timezone — it will be interpreted as UTC on reads, producing a 5-hour error. Store 2026-03-08T14:00:00Z (UTC) and let the presentation layer convert to local time.

Common timezone bugs and how to fix them

  • Meeting shifts by 1 hour twice a year: Calendar event stored with a fixed UTC offset (e.g., UTC-5) instead of an IANA zone. When EST → EDT, the offset changes but the stored value does not. Fix: store the IANA zone name alongside the UTC timestamp.
  • Log timestamps jump backward at 2 AM: Logs written in local time during a "fall back" DST transition produce duplicate timestamps. Fix: always log in UTC with a Z or +00:00 suffix.
  • "Tomorrow" is today for some users: A query for "records created today" runs at midnight UTC but some users are in UTC-8 — their "today" ends at 08:00 UTC. Fix: define "today" in the user's IANA zone, not UTC.
  • Age or duration miscalculated across DST: Subtracting two timestamps in a DST zone naively gives the wrong result during the 23-hour spring-forward day or the 25-hour fall-back day. Fix: always compute durations using UTC timestamps, never in local time.
  • API response parsed incorrectly: A JSON field "timestamp":"2026-03-08 09:00:00" with no offset suffix. Is it UTC? Local? Fix: always include an explicit offset or Z in API responses: "2026-03-08T09:00:00Z".

Debug timezone conversions manually with the live converter.

Open the converter →