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.
- IANA zone (
- 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/Parisis 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 (aClockdependency, notnow()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:timestamptzstores instants (it does not store the zone),timestampstores 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
- Unicode & text encoding — the sibling fundamental.
- How a computer works — the machine whose clocks and integers this topic leans on.
- Asynchronous and distributed system patterns — no-global-clock reasoning made formal.
- Databases and other storage systems — the storage types where these choices persist.
- Data engineering — event time vs. processing time is this topic at pipeline scale.