Dates, Times, and Time Zones

Expected level: Working

A point in time, a calendar date, a local clock reading and a time zone are four different things, and mixing them up causes some of the most persistent bugs in software.

The default rule

Store and transmit instants in UTC. Convert to a local time zone only when displaying to a person.

UTC (Coordinated Universal Time) does not have daylight saving, so a UTC timestamp identifies one exact moment everywhere and sorts and compares correctly. Local times are ambiguous: when clocks go back, a time such as 01:30 happens twice, and when they go forward, some times never happen.

Instants, offsets, zones and dates

  • Instant: an exact moment, such as when an order was placed. Store as UTC.
  • Offset: a fixed difference from UTC, such as +02:00. It does not say which rules apply on other dates.
  • Time zone: a named region with rules for offsets and daylight saving, such as Europe/Oslo. Use the IANA names; abbreviations like “CST” are ambiguous.
  • A date without a time, such as a birthday or a due date, has no time zone. Do not turn it into midnight UTC, or it can show as the previous day elsewhere.
  • A future local event such as “the meeting is at 09:00 Oslo time” should be stored as local time plus zone name, because the rules for that zone can change.

Formats

Use ISO 8601 / RFC 3339 text for exchange: 2025-03-30T14:05:00Z. The Z means UTC, and an offset such as +02:00 is also allowed. Avoid formats like 03/04/2025, which is read differently in different countries.

# Python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

now = datetime.now(timezone.utc)                      # timezone-aware UTC
local = now.astimezone(ZoneInfo("Europe/Oslo"))       # convert for display
print(now.isoformat())

Common date bugs

  • Naive datetimes, which have no zone attached, are silently treated as local time by some code and as UTC by other code.
  • Adding 24 hours does not always give “tomorrow”. On a daylight saving day, a day can be 23 or 25 hours long.
  • Do not rely on the server’s own time zone setting. Set it to UTC and be explicit in code.
  • Parsing differs between inputs. In JavaScript, new Date("2025-03-10") is read as UTC, while new Date("2025-03-10T00:00") is read as local time, and months in the constructor start at 0.
  • Clocks drift, so do not assume two machines agree to the millisecond.
  • Use a library. Never write your own calendar arithmetic.

Try it

Using Python or JavaScript, take the UTC timestamp 2025-03-30T00:30:00Z and display it in Europe/Oslo and in America/New_York. Then add 24 hours of elapsed time (add it in UTC, then convert), and compare with moving to the same local time on the next calendar day. Clocks in Oslo changed that night. (On Windows, Python may need pip install tzdata for named zones.)

Common mistakes

  • Using naive datetimes that carry no zone.
  • Adding 24 hours and expecting tomorrow, when daylight saving days are 23 or 25 hours.
  • Storing a date such as a birthday as midnight UTC.
  • Relying on the server's own time zone setting.

Check yourself

  • Why is a local time such as 01:30 ambiguous?
  • How should you store a future meeting at 09:00 Oslo time?
  • Why is 03/04/2025 a poor exchange format?

Related topics

Further reading