Time
Unix Time and Time Zones: The Mental Model That Prevents Date Bugs
· 3 min read
Date and time handling has a reputation as a graveyard of bugs, but nearly all of them trace back to one conflation: treating an instant (a point on the universal timeline) and a wall-clock time (what a clock on someone's wall reads) as the same kind of thing. They are not, and every robust system keeps them separate.
Unix time is the instant side of that divide: a single number counting seconds since 1970-01-01T00:00:00 UTC — the epoch. 1783150200 identifies one moment, identically, everywhere on Earth. Wall-clock times like '2026-07-08 14:30' are the human side, and they are meaningless without a timezone attached: that string names a different instant in Bangkok than in Berlin.
The rule that solves 90% of it
Store and compute with instants (Unix timestamps or UTC ISO 8601 with offset); convert to local wall-clock time only at the display edge. This single discipline eliminates ambiguity in databases, makes cross-region data comparable, and confines timezone complexity to the UI layer where it belongs.
The corollary for serialization: never write a datetime string without an offset. '2026-07-08T14:30:00' is a bug waiting for a reader in another zone; '2026-07-08T14:30:00+07:00' and '2026-07-08T07:30:00Z' are unambiguous. ISO 8601's Z suffix means UTC exactly, and every mainstream language parses it.
Units: the factor-of-1000 industry
Unix convention counts seconds; JavaScript and Java count milliseconds; databases and tracing systems use micro- and nanoseconds. Mixing them produces unmistakable symptoms — dates in January 1970 (seconds fed to a milliseconds API) or in the year 58,000 (the reverse) — and the diagnosis is a digit count: 10 digits = seconds, 13 = ms, 16 = µs, 19 = ns for contemporary dates.
JWT exp is seconds; Date.now() is milliseconds; the two meet in every auth bug tracker on the internet. Naming fields with units (expires_at_ms) is the cheap habit that makes the mismatch visible in code review instead of in production.
1783150200 → 10 digits: seconds 1783150200000 → 13 digits: milliseconds 1783150200000000 → 16 digits: microseconds new Date(1783150200) // Jan 21, 1970 — bug new Date(1783150200 * 1000) // correct
Time zones are political, offsets are not
An offset (+07:00) is arithmetic. A timezone (Asia/Bangkok, America/New_York) is a political history: a region's full record of offsets, DST rules, and legislative changes, maintained in the IANA tz database and updated several times a year as governments change their minds. The distinction matters for the future: 'next March 15th at 09:00 in New York' cannot be safely stored as an offset, because the DST rule that determines the offset could change between now and then. Store future wall-clock intentions as local time + IANA zone; store past events as instants.
Daylight saving creates two edge cases with concrete consequences: on spring-forward night an hour of wall-clock time does not exist (schedulers skip jobs placed there; parsers must decide what 02:30 means), and on fall-back night an hour happens twice (naive 'daily' jobs run twice; log timestamps go backwards unless they are UTC). 'A day is 86,400 seconds' is false twice a year in DST zones — date arithmetic should use calendar-aware libraries, not seconds math.
Assorted sharp edges worth knowing
- Year 2038: 32-bit signed second counters overflow on 2038-01-19. Solved on 64-bit systems, alive in embedded devices and MySQL's legacy TIMESTAMP column type.
- Leap seconds: Unix time pretends they don't exist (days are always 86,400 s); providers smear them across the day. Fine for business software, disqualifying for astronomy.
- JavaScript parsing: new Date('2026-07-08') is UTC midnight but new Date('2026/07/08') is local midnight — a one-character difference that shifts dates by a day for half the world.
- Zero and negative timestamps are valid (the epoch itself; pre-1970 dates). Code treating 0 as 'no date' misfiles real data.
- Client clocks lie: browsers and mobile devices routinely drift minutes; anything security-relevant (token expiry, rate limits) must trust only server time and allow small skew.
A debugging workflow
When a date is wrong, resolve three questions in order. What is the raw value? (Get the number or string before any formatting.) What unit and zone is it actually in? (Count digits; look for an offset.) Where was it converted? (Every boundary — JSON parse, ORM load, chart library — is a suspect.) Nine times out of ten the answer falls out at step two, which takes seconds with a converter in front of you.