viewer.csslab.dev

Telling seconds from milliseconds in a timestamp

Count the digits. For any date in the current era, a timestamp in seconds has ten digits, milliseconds has thirteen, and microseconds has sixteen. There is no flag in the value itself and no convention that all systems follow, so the magnitude is the only signal — which is why a value that is a thousand times too large produces a date around the year 55000 rather than an error.

Why there are three units at all

Unix time was defined in seconds, and that is what POSIX, most C APIs, JWT claims, and database epoch columns still use. JavaScript's Date.now() returns milliseconds, so anything that passed through a browser or Node tends to be in milliseconds. Logging and tracing systems often want finer resolution and use microseconds or nanoseconds.

None of these are wrong. The problem is that they all get called "a timestamp", and the value carries no indication of which one it is. A number is just a number.

Where it actually breaks

JWT is the common case. RFC 7519 defines exp, iat, and nbf as NumericDate, which is seconds. Issue a token with Date.now() and the expiry is a thousand times too far away — the token never expires, and it will not look broken until someone checks why a revoked session still works.

The reverse produces the more visible failure: a millisecond value divided somewhere and read as seconds lands in 1970. A date of 1970-01-01 in a record is almost never real data; it is a zero, a missing value, or a unit mismatch.

Sorting is the quiet one. Mix seconds and milliseconds in the same column and every millisecond value sorts after every second value regardless of when either happened, because 1700000000000 is larger than 1799999999.

What to do about it

At a boundary you control, put the unit in the name: expires_at_seconds, created_at_ms. This costs nothing and removes the question permanently.

Better still, send an ISO 8601 string — 2026-09-05T12:00:00Z — where the format itself says what the value is and no arithmetic can silently reinterpret it. Use an epoch integer where you need to compare or store compactly, and a string where a human or another system has to read it.

When you are receiving a value you did not choose, check the digit count before doing anything with it, and reject values that fall outside a plausible range rather than converting them. A timestamp from 1970 or from the year 50000 is a bug report, not a date.

Tools for this

Specifications this follows

Updated 2026-09-05