The rules in brief
- Store moments that have already happened in UTC.
- Store future events in the local time the person chose, plus an IANA zone ID.
- Save zone IDs like America/New_York, never abbreviations or fixed offsets.
- Convert to local time only when you display it.
- Use your platform's time zone library, and keep its data up to date.
- Test around clock changes, half-hour zones and the date line.
Past moments: store UTC
A log line, a payment or a message sent is an instant: one point on the world's timeline. Store it in UTC, or as a number of seconds since the Unix epoch, and it means the same thing on every server and in every country.
When you write an instant as text, use a format that carries its offset, such as RFC 3339: 2026-01-15T15:00:00Z, where Z means UTC, or 2026-01-15T10:00:00-05:00. A timestamp without an offset is a guess waiting to go wrong.[1]
Store zone IDs, not offsets or abbreviations
A user's time zone is a place, not a number. Store it as an IANA ID like Europe/Berlin or America/New_York. An ID carries the place's whole history of offsets and daylight saving rules. An offset like -05:00 is only right for part of the year, and abbreviations are ambiguous: CST means one thing in China and another in North America.[2]
In a browser, Intl.DateTimeFormat().resolvedOptions().timeZone gives you the visitor's zone ID, which is a good default to save with their account.
Future events: store what the person chose
Storing everything in UTC breaks for events in the future. If someone books a meeting for 9:00 in São Paulo next year, the UTC equivalent is only a prediction, based on today's rules. If the rules change before the day, the UTC value is now wrong, and the meeting shows an hour off.[3]
This really happens. Brazil, for example, abolished daylight saving time in 2019.[4] Any future meeting stored only in UTC under the old rules would have ended up at the wrong local time.
The safer design is to store what the person actually chose, the local date and time plus the zone ID, and work out the UTC instant from those whenever you need it. You can keep a UTC copy for sorting and queries, but recalculate it when the time zone data is updated.[3]
Convert at the edges
Keep times as instants inside your system, and only turn them into local time for display. Every modern platform can do the conversion using the IANA database:
const instant = new Date("2026-01-15T15:00:00Z");
new Intl.DateTimeFormat("en-GB", {
timeZone: "Asia/Tokyo",
dateStyle: "medium",
timeStyle: "short",
}).format(instant);
// "16 Jan 2026, 00:00"from datetime import datetime
from zoneinfo import ZoneInfo
when = datetime(2026, 1, 15, 15, 0, tzinfo=ZoneInfo("UTC"))
when.astimezone(ZoneInfo("Asia/Tokyo"))
# 2026-01-16 00:00:00+09:00Your code is only as current as its time zone data. Python's zoneinfo reads the operating system's database or the tzdata package, and some systems, notably Windows, don't ship one, so add tzdata as a dependency.[5] Browsers and operating systems update their own data, so keep servers and runtimes patched.
In JavaScript, the newer Temporal API has a ZonedDateTime type built for this.[6] It isn't available in every browser yet, so check support or use a polyfill.[7]
A day isn't always 24 hours
"Add one day" and "add 24 hours" give different answers across a clock change. In New York, the clocks went forward on March 8, 2026:
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")
start = datetime(2026, 3, 7, 9, 0, tzinfo=ny)
start + timedelta(days=1)
# 2026-03-08 09:00:00-04:00 (same wall-clock time)
(start.astimezone(ZoneInfo("UTC")) + timedelta(hours=24)).astimezone(ny)
# 2026-03-08 10:00:00-04:00 (24 real hours later)Decide which one you mean. A daily reminder should usually keep the same wall-clock time. A 24-hour timeout should count real hours.
Skipped and repeated times
When clocks go forward, some local times never happen. When they go back, some happen twice. Your code has to decide what to do with them:
- Python uses the
foldattribute to pick between the two copies of a repeated time: 0 for the first, 1 for the second.[5] - Temporal takes a
disambiguationoption ("compatible", "earlier", "later" or "reject") when it turns a local time into an instant.[6]
For user-facing scheduling, the friendly choice is usually to move a skipped time forward and to use the first of a repeated time, then tell the user.
Leap seconds and Unix time
Unix time counts every day as exactly 86,400 seconds, so it skips over leap seconds.[8] For most applications that's fine, and no leap second has been added since 2016. If you need exact elapsed time across years, for science or finance, use a time source and library that account for them. The UTC vs GMT guide explains where leap seconds come from.
What to test
- Clock changes: the spring and autumn change dates in
America/New_YorkandEurope/London, which fall on different weekends. - Southern hemisphere:
Australia/Sydney, whose clocks change in the opposite direction. - Unusual offsets:
Asia/Kolkata(+5:30) andAsia/Kathmandu(+5:45). - The date line:
Pacific/Kiritimati(+14) andPacific/Pago_Pago(−11), a day apart at the same moment. - No daylight saving:
America/PhoenixandAsia/Tokyo.
Run your tests with the machine's own time zone set to something other than UTC, too. Bugs that read the server's local zone by accident hide on servers that happen to run in UTC. For the background, see how time zones work.
Sources
- 1.IETF, RFC 3339: Date and Time on the Internet: Timestamps. The timestamp format with Z or a numeric offset.
- 2.IANA, Theory and pragmatics of the tz code and data. Area/Location zone names, and why abbreviations like CST are ambiguous.
- 3.Jon Skeet, Storing UTC is not a silver bullet. Storing future events as local time plus a zone ID, with UTC derived from them.
- 4.Associated Press, via Anchorage Daily News, Brazil eliminated daylight saving time. It's having second thoughts. Brazil abolishing daylight saving time in 2019.
- 5.Python documentation, zoneinfo: IANA time zone support. Where zoneinfo gets its data, the tzdata package, and the fold attribute.
- 6.MDN, Temporal.ZonedDateTime. JavaScript's zone-aware date type and how it resolves skipped and repeated times.
- 7.MDN, Temporal. Temporal's limited browser availability.
- 8.The Open Group, POSIX.1-2024, section 4.19: Seconds Since the Epoch. Unix time treating every day as exactly 86,400 seconds.
Related pages
- How time zones work
guide · How offsets, boundaries and local rules decide the time in a place.
- UTC vs GMT
guide · How UTC and GMT differ, and when the difference matters.
- Daylight saving time
guide · What daylight saving time is, who uses it, and when clocks change.
- IANA time zone database
resource · What the IANA time zone database is, who runs it, how its zone names and releases work, and how to read any zone ID.