DevFormatLab
← Back to blog

Unix Timestamp vs ISO 8601 — When Developers Get Bitten

By DevFormatLab Editorial·7 min read
TimestampISO 8601Time zonesDSTUnixCalendar

Of every data type developers ship between systems, time is the only one that humans read subjectively. A wrong number, a wrong string, or a wrong boolean fails loudly the first time it is used. A wrong timestamp is invisible — it parses, sorts, and renders, but it is off by a factor of 1000, a few hours, or a calendar day, and the silent drift corrupts reports, deadlines, and time-bound API tokens. This post is the field manual for shipping time over a wire without losing a day (or an hour) of accuracy.

Seconds versus milliseconds is silently destructive

The first decimal place of any timestamp tells you which unit you are in. Unix epoch 1 754 773 560 000 ms is about 56 years before the same value interpreted as seconds. JavaScript's Date uses milliseconds; most back-end APIs and databases use seconds; some legacy systems use microseconds. There is no flag in the value itself that distinguishes the three; the difference is purely in the producer's convention, which the consumer must know.

The bug pattern that generates the most incidents:

  1. A backend stores createdAt as epoch milliseconds.
  2. A frontend reads the value, interprets it as epoch seconds, and constructs a Date 56 years in the past.
  3. Every "bug report" the frontend files is the user's account being 56 years old.

The fix at every system boundary: pick one unit (milliseconds are the safest because they fit in JavaScript's Date precision threshold), document it, and add a unit test that catches the wrong interpretation. The Timestamp tool on this site has an Auto mode that tries both interpretations and shows the sensible one; that is often the right design choice for end-user tools.

Time zones are the second silent corruption

A Unix epoch is unambiguous — it is the number of seconds (or milliseconds) since 1970-01-01 00:00:00 UTC, period. An ISO 8601 string with a timezone offset (2026-08-06T15:00:00+09:00) is also unambiguous. An ISO 8601 string without an offset (2026-08-06T15:00:00) is genuinely ambiguous — it is a wall-clock date that could be any timezone's local time, and the consumer has to guess.

The four safe patterns, in order of preference:

  1. Always carry the timezone. RFC 3339 requires it; ISO 8601 permits it; every mainstream format includes it. 2026-08-06T15:00:00+09:00 parses unambiguously to a single moment in time, every time.
  2. Always carry the offset for storage, even if you decide to render without one. PostgreSQL's timestamptz, MySQL's DATETIME with explicit TZ, ISO 8601 strings — all of these can carry the offset. Render without one only at presentation time, immediately before showing to a human.
  3. Never assume the local time zone. "The user is in Tokyo" is not stored anywhere by default — your server's timezone is what it is, and the user's is whatever they configured. Always store the moment, derive the local view at render time.
  4. Use IANA timezone identifiers, never city names. Asia/Tokyo, America/Los_Angeles, Europe/London — these are stable, follow DST rules automatically, and are understood by every programming language's datetime library. "Tokyo" is a string that no library knows what to do with; "JST" is technically a fixed offset (+09:00) without DST history and loses historical accuracy for any date before Japan's DST ended in 1951.

Daylight Saving Time, the perennial footgun

DST is the practice of advancing clocks by one hour for part of the year, and it is politically unstable. The United States changed its DST rules in 2007 and may change them again; the European Union voted to abolish DST in 2019 and postponed the change repeatedly; Russia abolished DST in 2011 and re-introduced it in 2014 before abolishing again. Any code that hardcodes "spring forward, fall back" is wrong for at least one country in any given year.

The only safe pattern is to delegate to your language's timezone library — which itself delegates to the IANA Time Zone Database — and never reimplement the rules. In JavaScript, the relevant primitives are Intl.DateTimeFormat for rendering and the timeZone option to the standard library's date constructors; in Python, zoneinfo.ZoneInfo; in Java, java.time.ZoneId; in C#, TimeZoneInfo; in Rust, chrono_tz; in Go, the standard time.LoadLocation. Each of these takes care of the DST transitions for you.

DST creates three surprising edges:

  1. The "spring forward" gap — a one-hour window when local time does not exist. 02:30 on the second Sunday of March in New York does not exist for the year 2026. Code that asks "what is the UTC offset at this wall-clock time" must return a result (the offset before the transition, usually) and document the assumption.
  2. The "fall back" overlap — a one-hour window when local time exists twice. 01:30 on the first Sunday of November in New York happens twice in 2026; the offset is different in each occurrence. Most systems pick one (typically the first occurrence, or the offset in force when the timestamp is constructed).
  3. Computing arithmetic across a DST transition. 23:30 on a Saturday before a spring forward, plus two hours, is what? The naive answer is "Sunday 01:30 the next day", but in most US timezones that answer is wrong because 02:00 jumps to 03:00 and the arithmetic lands on 03:30. The right question is "add 7,200,000 ms, then convert to local time"; never add 2 hours to a wall-clock time.

Calendars have years; seconds do not

Epoch seconds is excellent for instant-in-time arithmetic and useless for "what month is this in" — that depends on the timezone. ISO 8601 calendar strings (2026-W31-3 for the third day of week 31, or 2026-08-06 for a specific local date) are excellent for "this calendar day in this calendar" and useless for arithmetic. Pick the right level of abstraction for the question.

A useful rule: anything that crosses a server / client / region boundary should be a UTC instant (Unix epoch with explicit milliseconds, or ISO 8601 with explicit offset). Anything a human reads should be a local rendering of that instant, with timezone abbreviation attached. Calendar-only fields — a birthday, an "end of fiscal quarter" — should be ISO 8601 date-only strings (2026-08-06) with the timezone understood by context (you do not need an offset to know "August 6, 2026" is the day you take the kids to the park).

The four rules

  1. Always store the instant. Unix epoch with milliseconds is the safest choice. ISO 8601 with explicit timezone offset is the next safest.
  2. Always document the unit. Seconds vs milliseconds is silent and catastrophic; the cure is documentation and a unit test.
  3. Always carry timezone for wire transport. No exceptions, no wall-clock-only strings crossing a process boundary.
  4. Delegate timezone math to a library. Never reimplement DST. The IANA database is the source of truth; your library's wrapper around it is the right level of abstraction.

Closing

Time is the only data type where the wrong unit, the wrong zone, or the wrong calendar still sort correctly. The four rules above are short but they eliminate almost every time-related bug in production. Tools: the Timestamp tool for inspecting and converting between formats, the JSON Formatter for visually scanning timestamp-bearing payloads (hover the value to read the human date when the key looks time-shaped), and the IANA Time Zone Database for any server-side arithmetic that spans regions.

Related Tools