Last updated 2025-09-10
Practical rules for storing in UTC, displaying in local time, converting between timezones, and avoiding DST pitfalls.
// UTC epoch seconds -> Date -> Local string
const seconds = 1640995200; // 2022-01-01T00:00:00Z
const d = new Date(seconds * 1000);
console.log(d.toISOString()); // UTC
console.log(d.toLocaleString()); // Local time (based on system timezone)
const ts = 1640995200; // seconds, UTC
const date = new Date(ts * 1000);
const inNY = date.toLocaleString('en-US', { timeZone: 'America/New_York' });
const inTokyo = date.toLocaleString('en-US', { timeZone: 'Asia/Tokyo' });
console.log({ inNY, inTokyo });
// Around DST, local times can repeat or skip.
// Prefer storing UTC timestamps and rendering with explicit timezone.
function toZone(d, tz) {
return d.toLocaleString('en-US', { timeZone: tz });
}
const d1 = new Date('2021-03-14T01:30:00-05:00'); // US spring forward day
const d2 = new Date('2021-11-07T01:30:00-04:00'); // US fall back day
console.log(toZone(d1, 'America/New_York'));
console.log(toZone(d2, 'America/New_York'));
import datetime, zoneinfo
ts = 1640995200
utc_dt = datetime.datetime.utcfromtimestamp(ts).replace(tzinfo=datetime.timezone.utc)
local_dt = utc_dt.astimezone(zoneinfo.ZoneInfo('America/New_York'))
print(utc_dt.isoformat())
print(local_dt.isoformat())
from datetime import datetime, timezone
def parse_iso_to_utc(iso_str: str) -> datetime:
dt = datetime.fromisoformat(iso_str.replace('Z', '+00:00'))
if dt.tzinfo is None:
# treat naive input as UTC or reject based on your policy
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
print(parse_iso_to_utc('2022-01-01T00:00:00Z').isoformat())
Because local time depends on the timezone offset (and DST). The timestamp itself is UTC and constant.
Store UTC timestamps, and when event semantics matter, store the IANA timezone ID (e.g., "Europe/Berlin"). Offsets alone are not enough across DST changes.
In JS, use toLocaleString
with a timeZone
option. In Python, use zoneinfo.ZoneInfo
and astimezone
.