Unix Timestamps: Seconds, Milliseconds, and Time Zones

Published 2026-10-31

A Unix timestamp counts seconds since 1970-01-01T00:00:00 UTC. That is the entire definition, and it is blissfully time-zone-free — which is exactly why most timestamp bugs happen around the timestamp, not in it.

Seconds or milliseconds?

Digits Unit Roughly
10 seconds until 2286
13 milliseconds until 33658
16 microseconds —
19 nanoseconds —

The classic bug: JavaScript's Date.now() returns milliseconds, PHP's time() and Python's time.time() return seconds. Mix them and you get a date in 1970 or in the year 56000.

new Date(1767225600)      // 1970-01-21 — treated as milliseconds
new Date(1767225600 * 1000) // 2026-01-01 — correct

Rule of thumb: count the digits. 13 means milliseconds. Anything else needs checking.

Unix time has no time zone. Display does.

1767225600 is one instant. It is 2026-01-01 00:00 in UTC, 08:00 in Beijing, and 2025-12-31 19:00 in New York. All three are correct.

This is why "the timestamp is wrong" almost never means the timestamp is wrong — it means the interpretation is. Three layers are routinely confused:

  1. The stored value — an integer, no zone, no ambiguity.
  2. The API contract — is this UTC or local? ISO 8601 with an offset (2026-01-01T08:00:00+08:00) is unambiguous; 2026-01-01 08:00:00 is not.
  3. The display — the user's zone, which may differ from the server's.

The bug appears at the boundary between 2 and 3: a server writes local time without an offset, a client reads it as UTC, and every timestamp is off by eight hours. It looks fine in testing because the tester and the server are in the same zone.

The 2038 problem, twice

32-bit signed time runs out at 2^31 - 1 seconds = 2038-01-19 03:14:07 UTC. After that it wraps to 1901.

It is still real in two places:

  • 32-bit embedded systems — routers, set-top boxes, industrial controllers, anything running 32-bit Linux.
  • File formats and protocols — some older archive and database formats store 32-bit timestamps regardless of the platform.

Most modern servers are 64-bit and unaffected. The risk is at the edges: a firmware update that never came, a database column defined as INT rather than BIGINT.

If you are designing a schema now, store timestamps as 64-bit integers or a native timestamp type. The cost is four bytes; the alternative is a migration you cannot do remotely.

Converting without losing your mind

date -d @1767225600 on Linux, date -r 1767225600 on macOS. In JavaScript, new Date(ts * 1000).toISOString() gives UTC in ISO form — the format you should be storing and comparing.

Two practical habits:

  • Store UTC, display local. Every conversion happens at render time, never at write time.
  • Never store a formatted string as your primary time value. 2026-01-01 08:00 cannot be sorted across zones, cannot be compared, and cannot be converted back without guessing the offset.

Batch work

Renaming a hundred files by date, or checking an export where a whole column is timestamps, is where a single-value converter stops being useful. Batch Timestamp Converter takes a column of values, detects seconds vs milliseconds per value, and converts all of them at once — including mixed input, which is exactly what a messy export looks like.

To see the same instant in several cities (and spot the day-boundary cases), use Time Zone Converter. For "run this at 03:00 UTC, what cron expression is that", Cron Generator is faster than counting fields.

The checklist

  1. Count the digits: 10 = seconds, 13 = milliseconds.
  2. A timestamp has no zone; the display does.
  3. ISO 8601 with an offset is unambiguous — use it at API boundaries.
  4. Store 64-bit; the 2038 problem is still alive on small devices and in old formats.
  5. Never store the formatted string as the source of truth.