Timezone bugs are one of the few categories of software defect that reliably survive code review, pass all the tests written in the same timezone as the developer, and then break in production the moment a user in a different region touches the feature. Most of them trace back to a small number of misunderstandings about what a Unix timestamp actually is.
A Unix Timestamp Has No Timezone
A Unix timestamp is a count of seconds (or milliseconds, depending on the system) since January 1, 1970, 00:00:00 UTC — the "epoch." That's the entire definition. It is not "in" any timezone, the same way a distance in meters isn't "in" any particular ruler.
1735689600 → 2025-01-01T00:00:00Z (UTC)
The confusion almost always happens at the boundary where a timestamp gets converted to a human-readable date — that conversion requires a timezone, and the timestamp itself carries no opinion about which one to use. new Date(1735689600000) in JavaScript will display a different local date/time string depending on the timezone of the machine running that code, while the underlying instant in time it represents never changes.
const ts = 1735689600000
new Date(ts).toISOString() // "2025-01-01T00:00:00.000Z" — always the same
new Date(ts).toLocaleString() // varies by the machine's local timezone
You can experiment with this directly using our Timestamp Converter — converting the same epoch value and watching how the human-readable output only makes sense once you fix a timezone for it.
Store UTC, Convert at the Edge
The single rule that prevents the majority of timezone bugs: store and compute in UTC (or as a raw timestamp), and only convert to a local timezone at the point of display. The moment you store a "local time" string in a database without an accompanying timezone, you've thrown away information you can't reliably recover later — was 2026-03-09 02:30:00 in US Eastern time, and if so, does it even exist (this exact local time doesn't, during that year's spring-forward transition)?
-- Good: store the timestamp, timezone-agnostic
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- Postgres: normalizes to UTC internally
-- Risky: store a naive local time with no timezone info attached
created_at TIMESTAMP NOT NULL -- ambiguous the moment users span timezones
Almost every mature database, language, and framework has both a "timezone-aware" and "naive" datetime type. Default to the timezone-aware one even when your current user base is in a single timezone — the cost of migrating a naive-datetime schema later, once you have real historical data with ambiguous local times, is far higher than using the aware type from day one.
Daylight Saving Time Breaks More Than People Expect
DST isn't just "clocks move an hour" — it means some local times don't exist (the hour skipped in spring) and some local times happen twice (the hour repeated in fall). Any code that does arithmetic on local datetimes — "schedule this job for 2:30 AM every day" — needs to account for the fact that "2:30 AM" is ambiguous or nonexistent on the transition days themselves. This is precisely why storing UTC and converting only for display sidesteps an entire category of bugs: UTC has no daylight saving time, ever.
Milliseconds vs Seconds — A Frequent, Silent Bug
JavaScript's Date constructor and Date.now() work in milliseconds since the epoch. Many backend languages and Unix tools (date +%s in a shell, many SQL functions) work in seconds. Passing a seconds-based timestamp into JavaScript's Date without multiplying by 1000 doesn't throw an error — it silently produces a date in 1970:
new Date(1735689600) // Wrong input scale — resolves to a date in 1970
new Date(1735689600000) // Correct — 2025
This class of bug is particularly nasty because it fails silently rather than crashing, and the resulting date (some point in early 1970) is plausible enough at a glance to slip past a quick sanity check.
Comparing and Sorting Dates Across Timezones
Comparing raw timestamps (or UTC datetimes) is always safe and produces a correct chronological order, because there's no ambiguity in what instant each value represents. Comparing formatted local-time strings is not — "03/04/2026" sorts differently depending on whether it's interpreted as day-first or month-first, and two users' "same local time" strings from different timezones don't represent the same instant at all. Sort and compare on the underlying timestamp or UTC value; only format to local time for the final display layer.
Summary
- A Unix timestamp has no timezone — it's a count of seconds/milliseconds since a single fixed UTC instant; timezone only enters the picture during display conversion.
- Store timestamps in UTC (or a timezone-aware type) and convert to local time only at the point of display — never store naive local datetime strings.
- DST means some local times don't exist and others repeat — a reason on its own to avoid scheduling or arithmetic on local time directly.
- Watch for seconds-vs-milliseconds mismatches; they fail silently rather than erroring. Use our Timestamp Converter to sanity-check a value before trusting it.