rgoussu@goussu: ~/library/fundamentals
~/library/fundamentals cat time-timezones-and-datetime.md

Time, timezones & datetime

# Instants, civil time, and the tzdb — storing time correctly, surviving DST, and the clock model under distributed systems.

Conceptsaved 2026-08-08updated 2026-08-09 #time#timezones#datetime#utc#fundamentals

Overview

Time bugs are a rite of passage because "what time is it" conflates three different types: an instant (a point on the physical timeline), civil/wall time (what a clock on a wall in some place reads), and a timezone (the politically-defined mapping between them, which changes). Modern datetime libraries (java.time, Temporal, chrono) encode this type distinction; most bugs are using the wrong type — or trusting the mapping to be stable, when it is neither fixed, nor reversible, nor gap-free.

Key points

  • The type system of time: instant (UTC timestamp), local date/time (no zone — "2026-12-25 09:00" means nothing globally), zoned datetime (local + zone = usually an instant), duration (physical seconds) vs. period ("one month" — calendar arithmetic). Use the narrowest honest type; converting early and precisely beats strings everywhere.
  • Store UTC — with the famous exception: past events are instants, store UTC. But future civil events (a 9:00 meeting in Paris next June) must store local time
    • IANA zone (Europe/Paris, never a fixed offset, never "CET"), because the UTC↔local mapping can change between now and then (governments do this constantly — the tzdb ships several updates a year). Converting a future meeting to UTC at write time silently bakes in today's rules.
  • DST makes local time non-continuous and ambiguous: spring-forward creates a gap (2:30 doesn't exist), fall-back an overlap (2:30 happens twice) — every scheduler and recurring job hits these; libraries let you choose resolution strategies, and "daily at 2:30 local" needs one. Never do arithmetic in local time ("add 24h" ≠ "same time tomorrow" across a transition — that's a period, not a duration).
  • Offsets are not zones: +02:00 is a moment's offset; Europe/Paris is a rule set with history. ISO 8601 with offset (2026-08-08T10:00:00+02:00) pinpoints an instant but does not preserve the zone — round-tripping civil semantics needs the zone name (RFC 9557 extends the format for exactly this).
  • The machine's clocks: wall clocks jump (NTP corrections, manual changes) — never measure elapsed time with them; that's the monotonic clock's job (System.nanoTime, CLOCK_MONOTONIC). Across machines, clocks disagree despite NTP — the reason distributed systems reason with logical/hybrid clocks instead of timestamps (async patterns); leap seconds exist and clouds smear them.
  • Practical hygiene: test with zones like Pacific/Kiritimati (UTC+14), half-hour zones (India), and DST edges; inject the clock (a Clock dependency, not now() scattered) so tests control time (testing strategies); format for users with locale-aware formatters at the edge — the ISO string is for machines. Database types: timestamptz stores instants (it does not store the zone), timestamp stores civil time — teams that don't know the difference are currently paged.
  • To explore: recurring-event modeling (RRULE), calendar arithmetic edge cases (Jan 31 + 1 month), TAI vs. UTC, why aviation runs on UTC.

Practice

  • "UTC is enough for everyone… right?" (source) — a guided tour of the traps; read it first, then go break something on purpose.
  • DST-safe scheduler (exercise) — hand-parse RFC 3339, store a future meeting that survives a tzdb update, and expand "daily at 2:30" across both DST edges — this note's drills consolidated into one build with acceptance checks.

Related