All guides

UTC, ISO 8601 and time zones: avoiding the most common date bugs

Updated

A Unix timestamp has no time zone — it identifies an instant, and that instant is the same everywhere. Time zones only enter the picture when you turn that instant into a calendar date and a clock time for a person to read. Most date bugs come from blurring that line.

UTC: the reference clock

Coordinated Universal Time (UTC) is the global time standard. It doesn’t observe daylight saving and is, for everyday purposes, the same as GMT. Every local time zone is defined as an offset from UTC: London is UTC+0 in winter and UTC+1 in summer, New York is UTC−5 in winter and UTC−4 in summer, India is UTC+5:30 all year.

Unix time is anchored to UTC. The timestamp 1735689600 is 00:00 on 1 January 2025 in UTC — which was also 00:00 in London, 19:00 on 31 December in New York, and 05:30 on 1 January in Mumbai. Same instant, four different wall-clock readings.

Reading ISO 8601 and RFC 3339

ISO 8601 is the international standard for writing dates and times as text, and RFC 3339 is the stricter profile of it used across the internet. The format runs from largest to smallest unit, so strings sort correctly as text:

Examples
2025-01-01T00:00:00Z          UTC ("Z" = zero offset, "Zulu")
2025-01-01T00:00:00+00:00     the same instant, explicit offset
2024-12-31T19:00:00-05:00     the same instant, written in New York winter time
2025-01-01T05:30:00+05:30     the same instant, written in India time
2025-01-01T00:00:00.250Z      with milliseconds

The offset at the end is what makes the string unambiguous. A string with no offset or Z — like 2025-01-01T00:00:00 — is a local time with no indication of which locality, and different software will interpret it differently.

An offset is not a time zone

+01:00 tells you the offset at one instant. A time zone such as Europe/London is a set of rules that says which offset applies when, including daylight saving changes and historical changes. If you need to schedule something “every day at 9:00 London time”, store the zone name (from the IANA time zone database), not today’s offset — the offset will be wrong half the year.

Daylight saving time traps

  • Some local times don’t exist. In the UK, clocks jump from 01:00 to 02:00 on the last Sunday of March, so 01:30 that morning never happens.
  • Some local times happen twice. On the last Sunday of October, 01:00–02:00 repeats, so “01:30” is ambiguous without an offset.
  • Not every day is 24 hours long in local time. Adding 86,400 seconds to “midnight local” across a DST change lands on 23:00 or 01:00. Use your language’s calendar arithmetic (“add one day”) when you mean calendar days.
  • Offsets aren’t always whole hours. Nepal is UTC+5:45; some Pacific islands are as far as UTC+14.

A JavaScript gotcha

JavaScript parses date-only and date-time ISO strings differently:

JavaScript
new Date("2025-01-01")        // treated as UTC midnight
new Date("2025-01-01T00:00")  // treated as LOCAL midnight

In a browser set to New York, the first shows 31 December at 19:00 and the second shows 1 January at 00:00. Strings in other formats (like "01/02/2025") are parsed in implementation-specific ways and should be avoided entirely. Include an explicit Z or offset whenever you can.

Rules of thumb

  1. Store and transmit instants as UTC — either a Unix timestamp or an ISO 8601 string ending in Z.
  2. Convert to local time only at the edge, when displaying to a person.
  3. For future scheduled events in local time, store the local time plus an IANA zone name.
  4. Always include an offset in timestamp strings; never rely on the server’s local zone.
  5. Keep servers and logs on UTC so events from different machines line up.