Unix Timestamps: Seconds, Milliseconds, and Time Zones

By ··7 min read·timestampsunixdatetime

Why the same number can mean 1970 or the year 55000, how to tell seconds from milliseconds, and where time-zone bugs actually come from when you convert an epoch to a human-readable date.

A Unix timestamp is one of the simplest ideas in computing: count the number of seconds that have elapsed since midnight UTC on 1 January 1970, and you have an unambiguous, time-zone-free instant. It is the format you find in HTTP headers, log lines, JWT exp claims, database columns, and almost every API that needs to express "when did this happen." And yet timestamps are a reliable source of bugs, almost always for one of three reasons: the unit is ambiguous, the epoch is misread, or a time zone sneaks in where it should not.

Seconds or milliseconds?

The original Unix convention is seconds since the epoch. But JavaScript's Date.now() returns milliseconds, and so do many JVM and .NET APIs. The two differ by a factor of 1000, which means a value interpreted in the wrong unit is not slightly off — it is off by three orders of magnitude. Read a millisecond timestamp as seconds and a 2026 date becomes the year 55000; read a second timestamp as milliseconds and it collapses to a few minutes after the epoch, in January 1970.

There is no bit in the number that tells you which unit it is, so you have to infer it from magnitude. A useful rule of thumb for any date in the current era:

  • ~10 digits (e.g. 1752566400) → seconds. A 10-digit value covers roughly the years 2001 through 2286.
  • ~13 digits (e.g. 1752566400000) → milliseconds.
  • 16 or 19 digits → microseconds or nanoseconds, common in tracing systems, databases like ClickHouse, and Go's time.UnixNano().

This is exactly the heuristic the Timestamp Converter uses: it looks at the size of the number, guesses the unit, and shows you the resulting date so you can sanity-check it at a glance. If the date comes out as 1970 or some absurd far-future year, the unit guess was wrong — and that is almost always the bug.

The epoch is UTC — the display is not

A Unix timestamp has no time zone. It is a count of seconds from a fixed instant in UTC, full stop. Time zones only enter the picture when you convert that instant into a human-readable calendar date and clock time, because "3:00" means different absolute instants in Tokyo, London, and New York.

Most timestamp bugs are really display bugs. The stored number is correct, but somewhere in the pipeline a library formatted it in the server's local zone instead of UTC, or parsed a local date string as if it were UTC. The symptom is an event that appears to happen a few hours before or after it actually did — and the offset conveniently matches someone's time-zone difference from UTC.

The defensive habit: store and transmit instants as UTC epoch values (or ISO-8601 strings with an explicit Z or offset), and convert to local time only at the very edge, when you render for a human. Never do arithmetic on local wall-clock times.

Off-by-one-hour: daylight saving time

Even when you keep everything in UTC internally, DST bites at the conversion boundary. Adding "one day" to a local timestamp is not always adding 86,400 seconds: on the days a region springs forward or falls back, a local day is 23 or 25 hours long. If you compute "tomorrow at 9am" by adding 86,400 seconds to a UTC instant and then formatting locally, you will be an hour off twice a year.

The fix is to do calendar arithmetic with a real date library that is zone-aware (one backed by the IANA tz database), rather than treating a day as a fixed number of seconds. Epoch math is correct for measuringdurations; it is wrong for reasoning about calendars.

The year 2038 problem

Systems that store the timestamp in a signed 32-bit integer overflow on 19 January 2038, when the second count exceeds 2,147,483,647. After that the value wraps to a negative number and the date jumps back to 1901. This is not hypothetical — it still lurks in old C code, embedded firmware, and a few database column types. The remedy is a 64-bit integer, which pushes the overflow roughly 292 billion years out. If you control the schema, store epoch values in a 64-bit column and never think about it again.

Leap seconds, briefly

Unix time deliberately ignores leap seconds: it assumes every day is exactly 86,400 seconds. Real UTC occasionally inserts a leap second to stay aligned with the Earth's rotation. For almost all application code this discrepancy is invisible and you should not try to correct for it — the operating system and NTP smear the difference for you. It matters only for high-precision scientific timing, and if you are in that world you already know it.

A practical checklist

  • Confirm the unit before you convert. Ten digits is seconds; thirteen is milliseconds. If the date looks like 1970, you guessed wrong.
  • Store and pass instants in UTC. Convert to local time only when rendering for a person.
  • Use a zone-aware date library for calendar arithmetic; use plain epoch math only for durations.
  • Use 64-bit storage for timestamps to sidestep the 2038 problem.

When you are staring at a raw epoch value in a log and need to know what instant it actually represents, paste it into the Timestamp Converter — it detects the unit, shows both the UTC and local rendering, and lets you go the other way to turn a date back into an epoch for a query or a test fixture.