foundations · level 7

Time & Timezones

UTC, ISO-8601, DST, leap seconds, and why naive datetime is a bug bomb.

175 XP

Time & Timezones

If "naive datetime" is in your codebase, you have a bug — you just don't know it yet. Of all the silent failure modes in software, time-related ones are the most reliably catastrophic and the easiest to prevent.

Analogy

Storing a datetime without a timezone is like writing "the meeting is at 3" on a sticky note and leaving it on a table in an airport. Three at JFK and three at LAX are real moments three hours apart, and your sticky note can't tell which one you meant. The fix isn't to add timezones at the moment of confusion — it's to never write the sticky note without one in the first place. UTC is the airport-standard timezone everyone agrees on. Wall-clock time is what you put on the meeting invite.

The two kinds of time

There are exactly two things people mean when they say "datetime":

  1. An instant — a specific moment in real-world time. "When was the last login?" "When did the order ship?" These should always be stored in UTC and converted at display time.
  2. A wall-clock time — what a clock on a wall says, with no specific instant. "The store opens at 9am every day." "Run the report at 02:30 every morning." These are intrinsically tied to a timezone, not to UTC.

Mixing them is the source of 90% of time bugs. Login timestamps stored as wall-clock time fall apart when you cross DST or move servers. Wall-clock schedules stored as UTC drift twice a year as DST shifts.

UTC — the universal coordinate

UTC is the reference timezone. It does not observe DST. It does not depend on where the server lives. It is what every other timezone is defined in terms of.

Rule of thumb: every datetime stored in your database should be in UTC unless you have a specific reason it shouldn't be — and "specific" means you can articulate the wall-clock-time use case in a sentence.

The pattern engineers reach for is store UTC, display local:

[user enters time]  →  [convert to UTC]  →  [store UTC]  →  ...  →  [read UTC]  →  [convert to user tz]  →  [display]

Conversion happens at the boundaries (entry and display), not in storage or transit.

ISO-8601 / RFC 3339

The interchange format. Two equivalent forms:

2026-04-28T14:30:00Z              # Z = UTC
2026-04-28T14:30:00+00:00         # explicit zero offset
2026-04-29T02:30:00+12:00         # NZST (Auckland)

The trailing offset is non-negotiable — without it, the timestamp is ambiguous. RFC 3339 is the strict subset most APIs accept; ISO 8601 is a slightly larger superset. Use Z for UTC; use ±HH:MM for other offsets.

Anti-patterns:

2026-04-28 14:30:00      # missing 'T' separator AND no offset — ambiguous
04/28/2026 2:30 PM       # locale-dependent display format — never store this
1777904000               # Unix epoch is fine internally, but ISO-8601 is more readable in logs

The DST curse

Daylight saving time is a per-jurisdiction policy that shifts wall clocks forward in spring and back in autumn. The two consequences for code:

  • Spring forward — a 23-hour day. The hour 02:00–03:00 simply doesn't exist locally. Cron jobs scheduled for 0 2 * * * will not run that day.
  • Fall back — a 25-hour day. The hour 01:00–02:00 happens twice. A cron job scheduled for 0 1 * * * may run twice.

UTC has no DST, which is the killer reason to store everything in UTC.

DST rules are also politically mutable. Brazil abolished DST in 2019. Iran abolished it in 2022. The EU has voted multiple times to abolish it but hasn't agreed on which timezone to lock in. The only correct way to track these changes is to use the IANA timezone database (tz/Olson), which gets updated several times a year. Never hardcode +12:00; store Pacific/Auckland and let the tz library figure out the offset for that instant.

Leap seconds

Earth's rotation is slowing down (very slightly). UTC is defined to stay within 0.9 seconds of solar time, so occasionally a leap second is inserted: 23:59:59 → 23:59:60 → 00:00:00.

Operationally:

  • Most software pretends leap seconds don't exist. Some smear them across a longer window (Google's "leap smear", 24h × +0.012ms).
  • A few high-frequency systems care a great deal — financial trading, GPS, scientific instruments.
  • The 2017-to-2022 era saw multiple leap-second-related production incidents (Cloudflare's 2017 outage was DNS-related leap second handling).

Leap seconds will likely be abolished by 2035 (a UN resolution passed in 2022). Until then, treat them as a known oddity that occasionally causes 0.1% of your engineers to lose a Friday.

Naive vs aware datetimes

Most languages distinguish:

  • Naive datetime — has year/month/day/hour/minute/second but no timezone. Useless for instants.
  • Aware datetime — has the above PLUS a timezone or offset. The only correct type for instants.

Python's datetime lets you make either; datetime(2026, 4, 28, 14, 30) is naive. Always pass tzinfo=timezone.utc (or a ZoneInfo).

JavaScript's Date is intrinsically UTC under the hood — new Date() is always UTC milliseconds since epoch. The display methods (toString) lie about the local timezone, which is why people get confused. .toISOString() always emits UTC.

Go's time.Time is always location-aware; you set it via time.UTC or time.LoadLocation("Pacific/Auckland").

Rust's chrono::DateTime<Utc> and DateTime<Local> are distinct types. The compiler refuses to mix them up.

Patterns to internalise

Pattern When
Store as UTC, display local Instants in time (created_at, updated_at, last login)
Store as wall-clock + tz Schedules ("9am every Monday Auckland time")
Store IANA tz name, not offset Anything that needs DST awareness
Use Instant/epoch for ordering When you need monotonic ordering across timezones
Always serialise as ISO-8601 with offset API requests/responses, logs

Common bugs and how to spot them

  • Off by 1 hour for half the year. Almost certainly DST handling. Probably a hardcoded offset somewhere.
  • Off by 12 hours always. AM/PM mixed up, or a sign error on the offset.
  • Off by exactly the local UTC offset. Storing local time as if it were UTC.
  • A datetime in the database that's "in the future" for one server and "in the past" for another. Naive datetime, two timezones.
  • Cron jobs running twice on a Sunday in November. DST fall back; switch the schedule to UTC.

Display formatting

Display is a separate concern from storage. Match the user's locale:

new Date().toLocaleString("en-NZ", { timeZone: "Pacific/Auckland" });
// 29/04/2026, 02:30:00

new Intl.DateTimeFormat("en-US", {
  dateStyle: "medium",
  timeStyle: "short",
  timeZone: "America/Los_Angeles",
}).format(new Date());
// Apr 28, 2026, 7:30 AM

Locale strings (en-NZ, de-DE) drive the formatting; timezone is separate. Don't conflate them — a German-speaking user in San Francisco wants German formatting in Pacific Time.

Testing time

The hardest thing to test is "what happens at the DST boundary?" Tools:

  • Freeze time. freezegun (Python), vi.useFakeTimers() (vitest/JS), clock.lock() (Go's time package interface).
  • Test multiple timezones. Don't run all your tests in UTC; run a subset in Pacific/Auckland (UTC+12/+13) and America/New_York to catch tz-dependent bugs.
  • Test on the boundary days. Pick datetimes within the DST shift hour. Make sure your code does the right thing.

What to internalise

  • Every datetime in your code is either an instant (store UTC) or a wall-clock time (store with explicit tz). There is no third option.
  • ISO-8601 with Z or explicit offset is the only safe interchange format.
  • Use IANA timezone names; never store fixed offsets.
  • DST is a political policy that changes; trust the tz database, not your memory.
  • Naive datetime is a bug bomb. The fix is always to add a timezone.

Tools in the wild

5 tools
  • tzdatafree tier

    The canonical timezone rules database. Bundled with most language runtimes.

    spec
  • luxonfree tier

    Modern JavaScript datetime library with first-class timezone and DST support.

    library
  • JavaScript's upcoming built-in datetime API — finally tz-aware.

    spec
  • pendulumfree tier

    Python datetime library with sane DST and tz arithmetic.

    library
  • Quick web tool to debug Unix timestamps.

    service