Converting a Unix timestamp to UTC is easy, because a timestamp is already a single instant (see Unix timestamps explained). The hard direction is the other one: turning "9:00 in New York" or a date string from a form into the right UTC instant. This guide shows how in JavaScript, Python and SQL, and what happens around daylight saving time (DST) changes. Every example was run on Node.js 24.21, Python 3.9 (zoneinfo) and SQLite 3.51.
Offsets and time zones are different things
- An offset such as
+07:00is a fixed difference from UTC.2026-10-28T14:00:00+07:00names exactly one instant. - A time zone such as
America/New_Yorkis a set of rules. Its offset is-05:00in winter and-04:00in summer, and governments change the rules from time to time.Asia/Phnom_Penhis+07:00all year, because Cambodia does not use DST.
So a wall-clock time plus an offset converts directly. A wall-clock time plus a zone name needs the zone's rules for that date, and a wall-clock time with neither is ambiguous.
Strings with an offset: the easy case
// A string with an offset (or Z) is unambiguous.
new Date("2026-10-28T14:00:00+07:00").toISOString();
// "2026-10-28T07:00:00.000Z"
// UTC instant -> wall-clock time in a named zone, for display.
new Date("2026-10-28T07:00:00Z").toLocaleString("en-GB", {
timeZone: "Asia/Phnom_Penh",
});
// "28/10/2026, 14:00:00"-- SQLite converts an ISO string with an offset to UTC
SELECT datetime('2026-10-28T14:00:00+07:00'); -- 2026-10-28 07:00:00
SELECT unixepoch('2026-10-28T14:00:00+07:00'); -- 1793170800
SELECT datetime(1793170800, 'unixepoch'); -- 2026-10-28 07:00:00SQLite has no time zone database. It understands offsets in the input, and its localtime and utc modifiers use the server's own zone, but it cannot convert to or from a named zone. Do that conversion in application code.
Strings without an offset depend on the machine
| Input | On a machine set to UTC | On a machine set to Asia/Phnom_Penh |
|---|---|---|
new Date("2026-10-28T14:00:00") | 2026-10-28T14:00:00.000Z | 2026-10-28T07:00:00.000Z |
new Date("2026-10-28") | 2026-10-28T00:00:00.000Z | 2026-10-28T00:00:00.000Z |
JavaScript treats a date-time without an offset as local time, but a date-only string as UTC. In Python, a "naive" datetime (no tzinfo) is also treated as local time by .astimezone(). The same code therefore gives different results on a laptop and on a server set to UTC. If a string has no offset, decide which zone it is in and convert it as shown next.
A wall-clock time in a named zone
Python
zoneinfo is in the standard library from Python 3.9:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# String with an offset -> UTC
datetime.fromisoformat("2026-10-28T14:00:00+07:00").astimezone(timezone.utc)
# 2026-10-28 07:00:00+00:00
# Wall-clock time in a named zone -> UTC
ny = ZoneInfo("America/New_York")
datetime(2026, 7, 1, 9, 0, tzinfo=ny).astimezone(timezone.utc)
# 2026-07-01 13:00:00+00:00
datetime(2026, 12, 1, 9, 0, tzinfo=ny).astimezone(timezone.utc)
# 2026-12-01 14:00:00+00:00
# UTC -> a named zone, for display
datetime(2026, 10, 28, 7, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Asia/Phnom_Penh"))
# 2026-10-28 14:00:00+07:00JavaScript
JavaScript can format an instant in any zone, but it has no built-in way to go from a zone's wall-clock time to UTC. The proposed Temporal API covers it, but it was not available in Node.js 24 (typeof Temporal is "undefined"). This function uses only Intl, and gave the same results as Python for every case in the DST table below:
// Offset (minutes east of UTC) of a time zone at a given instant.
function offsetMinutes(timeZone, ms) {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
}).formatToParts(ms);
const p = Object.fromEntries(parts.map((x) => [x.type, Number(x.value)]));
const wall = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
return (wall - Math.floor(ms / 1000) * 1000) / 60000;
}
// Wall-clock time in a time zone -> UTC Date. In a DST overlap it picks the
// earlier instant; in a DST gap it moves forward, like Python and Temporal.
function zonedTimeToUtc(year, month, day, hour, minute, timeZone) {
const wall = Date.UTC(year, month - 1, day, hour, minute);
const before = offsetMinutes(timeZone, wall - 86_400_000);
const after = offsetMinutes(timeZone, wall + 86_400_000);
const valid = [before, after]
.map((o) => wall - o * 60_000)
.filter((t) => t + offsetMinutes(timeZone, t) * 60_000 === wall);
return new Date(valid.length ? Math.min(...valid) : wall - before * 60_000);
}
zonedTimeToUtc(2026, 7, 1, 9, 0, "America/New_York").toISOString();
// "2026-07-01T13:00:00.000Z" (EDT, UTC-4)
zonedTimeToUtc(2026, 12, 1, 9, 0, "America/New_York").toISOString();
// "2026-12-01T14:00:00.000Z" (EST, UTC-5)Libraries such as Luxon or date-fns-tz do the same with more options. Whatever you use, test it with the DST dates below.
DST gaps and overlaps
When clocks go forward, some wall-clock times never happen. When they go back, some happen twice. Results from Python's zoneinfo and the JavaScript function above:
| Wall-clock time | What happens | Result (UTC) |
|---|---|---|
| 2026-03-08 02:30, America/New_York | Does not exist (02:00 jumps to 03:00) | 07:30Z, which is 03:30 local |
| 2026-11-01 01:30, America/New_York | Happens twice | 05:30Z (first, EDT); Python gives 06:30Z with fold=1 |
| 2026-03-29 01:30, Europe/London | Does not exist | 01:30Z, which is 02:30 local |
| 2026-10-25 01:30, Europe/London | Happens twice | 00:30Z (first, BST) |
| 2026-10-04 02:30, Australia/Sydney | Does not exist | 2026-10-03 16:30Z, which is 03:30 local |
If a user picks a time that does not exist or is ambiguous, it is usually better to tell them than to guess silently, for example in a scheduling form.
How to store times
- Things that happened (logs, orders, sign-ups): store the instant in UTC, as a Unix timestamp, a
timestamptzcolumn or an ISO string ending inZ. - Things planned in local time (a meeting at 09:00 in New York next year): store the wall-clock time and the zone name, and convert when needed. If the zone's rules change before the date, a UTC value saved today would be wrong.
- Display: convert from UTC to the viewer's zone at the last moment, and show the zone or offset when it matters.
To check a timestamp quickly in UTC and your local zone, use the timestamp converter.