Unix timestamps, ISO 8601, and the timezone bugs that haunt every codebase
A number counting seconds since 1970 sounds simple. Here's why time is the hardest 'simple' thing in software — and how to stop getting it wrong.
There's a running joke that every developer confidently thinks they understand time until they ship their first scheduling feature. Time seems simple — it's just a number going up — and then daylight saving time, leap seconds, timezone databases, and the difference between "when it happened" and "what the clock on the wall said" turn a two-hour task into a two-week saga of off-by-one-hour bugs.
Most of that pain comes from a handful of concepts. Get them straight and the category of "the timestamp is wrong" bugs mostly disappears.
The Unix timestamp: one number, one meaning
A Unix timestamp (also called epoch time) is the number of seconds that have elapsed since 00:00:00 UTC on January 1, 1970 — the "Unix epoch." Right now that's somewhere north of 1.7 billion.
1751990400 → 2026-07-08 16:00:00 UTC
Its great virtue is that it's unambiguous and universal. A Unix timestamp is
always in UTC — it has no timezone, no ambiguity, no formatting. The same
instant is the same number everywhere on Earth. Two servers on opposite sides of
the planet that record 1751990400 are talking about the exact same moment. This
is why timestamps are the right thing to store and transmit: they're a single
point on the universal timeline with zero interpretation required.
A few practical notes:
- Classic Unix time counts seconds. JavaScript's
Date.now()and many APIs use milliseconds, so the number is 1000× larger. The most common timestamp bug is mixing the two — passing seconds where milliseconds are expected puts you in 1970, and vice versa lands you 50,000 years in the future. When a date looks wildly wrong, check your units first. - The timestamp is a count, not a string.
1751990400carries no formatting; turning it into "July 8, 2026" is a separate rendering step.
The Year 2038 problem
A quick but important aside. Many older systems store Unix time in a signed 32-bit integer, which can only count up to 2,147,483,647 seconds — a value it reaches at 03:14:07 UTC on January 19, 2038. One second later it overflows and wraps around to a negative number, landing in December 1901. This is the "Y2038" problem, and it's a real reason modern systems use 64-bit timestamps (good for roughly 292 billion years). If you're choosing a storage type for time today, use a 64-bit integer or a proper timestamp type — never a 32-bit int.
ISO 8601: the format for humans and APIs
If timestamps are for storage, ISO 8601 is the format for exchange and display in a structured way. It writes a moment as:
2026-07-08T16:00:00Z
Year-month-day, a T separator, then the time, then a timezone designator. The
Z (for "Zulu") means UTC; an offset like +09:00 means nine hours ahead of UTC
(Seoul, in this case). ISO 8601 has three things going for it:
- It's unambiguous. Unlike
07/08/2026— which is July 8th in the US and August 7th in most of Europe — ISO order is always big-endian: year, then month, then day. No one misreads it. - It sorts correctly as plain text. Because it goes from largest unit to smallest, alphabetical sorting of ISO strings is chronological sorting. This is quietly wonderful for logs and filenames.
- It carries its timezone, so unlike a bare "16:00" it's a real, locatable instant.
The pairing that works well in practice: store and compute in Unix time (or UTC internally), serialize to ISO 8601 at your API boundary, and format to the user's local style only at the very last moment, in the UI.
The core rule: UTC everywhere, local only at the edges
Almost every timezone bug traces back to violating one principle: store and process time in UTC; convert to local time only for display.
When you accept a time from a user, convert it to UTC immediately. Store UTC. Do all your comparisons, sorting, and scheduling in UTC. Then, and only then — when you render something a human will read — convert to their timezone. If you keep local times floating around your database and business logic, you will eventually compare two times from different zones and get a wrong answer, and it'll surface as a bug that only reproduces for users in certain countries or during certain months.
Why timezones are genuinely hard
It's tempting to think a timezone is "just an offset" — UTC+9, UTC−5. It isn't, and this is where the real difficulty lives.
Daylight saving time. Much of the world shifts its clocks twice a year. So "US Eastern" is UTC−5 in winter and UTC−4 in summer. A fixed offset can't capture this. Worse, DST transitions create genuinely broken moments: when clocks "spring forward," the local time 2:30am does not exist on that day; when they "fall back," 1:30am happens twice. Scheduling a daily job at 2:30am local time means one day a year it runs zero times, and another day it runs twice.
The offset depends on the date. Because of DST — and because governments change
their timezone rules with startling frequency — the correct offset for a given
timezone depends on which instant you're asking about. This is why you can't hard
-code offsets. You need the IANA timezone database (the "tz database"), the
crowd-maintained record of every region's historical and current rules, identified
by names like America/New_York or Asia/Seoul. Store a user's timezone as an
IANA name, never as a raw offset, so the offset can be computed correctly for any
date.
Wall-clock intent vs. instant. Sometimes you want a specific instant ("the launch happens at this exact global moment") and sometimes you want a wall -clock time ("remind me at 9am wherever I am"). These need different handling: the first is a fixed UTC point; the second is a local time that resolves to different instants depending on the user's zone and the date. Deciding which one you mean is half the battle.
Converting and sanity-checking
When you're debugging — "why does this record say it was created at 4pm when the user swears it was 1pm?" — the fastest move is to take the raw stored value and convert it explicitly, checking the units and the timezone at each step. A Timestamp Converter lets you paste a Unix timestamp and see the corresponding UTC and local datetime (and go the other direction), which immediately reveals the usual culprits: seconds-vs-milliseconds confusion, or a UTC value being displayed as if it were local.
The takeaway
Store time as a Unix timestamp or UTC — one unambiguous number on the universal timeline — and reserve local time for display only, converting at the very edge of your system. Exchange structured times as ISO 8601, which is unambiguous and sorts correctly. Watch the two perennial traps: seconds vs. milliseconds (the cause of most "wildly wrong date" bugs) and timezones as offsets (they're not — use IANA names so DST is handled correctly). Respect those and time becomes merely tedious, instead of genuinely cursed.