Why milliseconds exist at all
Whole seconds are too coarse for anything that measures itself. A web request, a database query and
a garbage-collection pause all finish inside one second, so a seconds-resolution clock reports them
as taking zero time. Two ecosystems solved this by making milliseconds the base unit of their clock
rather than a decimal on top of it: JavaScript, where Date is internally a millisecond
count, and Java, where System.currentTimeMillis() has been the standard call since
version 1.0.
Almost everything else kept seconds and added a fraction. Python's time.time() returns
a float; PostgreSQL's EXTRACT(EPOCH …) returns a numeric with decimals; Go added
UnixMilli() as a separate call alongside Unix(). So the boundary
between a JavaScript or Java service and anything else is a factor of 1000 — and because both sides
just see an integer, nothing complains.
The same digits, read both ways
This is the table to bookmark. Each row takes one literal integer of the kind you actually find in logs and shows what it means under both readings, so the cost of guessing wrong is a date rather than a warning.
| Literal | Digits | Comes from | Read as seconds | Read as milliseconds |
|---|---|---|---|---|
1700000000 | 10 → seconds | Python time.time(), Go time.Now().Unix(), PHP time(), date +%s | 2023-11-14T22:13:20.000Z | 1970-01-20T16:13:20.000Z |
1000000000 | 10 → seconds | the first ever 10-digit timestamp — common in test fixtures | 2001-09-09T01:46:40.000Z | 1970-01-12T13:46:40.000Z |
1700000000000 | 13 → milliseconds | JavaScript Date.now(), Java System.currentTimeMillis() | +055840-11-08T22:13:20.000Z | 2023-11-14T22:13:20.000Z |
1709164800000 | 13 → milliseconds | a Kafka record timestamp on the 2024 leap day | +056131-04-12T00:00:00.000Z | 2024-02-29T00:00:00.000Z |
1700000000000000 | 16 → microseconds | Python datetime microseconds, Chrome trace events, PostgreSQL internals | not a representable date | +055840-11-08T22:13:20.000Z |
Three patterns fall out of that table, and between them they identify almost every unit bug you will meet:
- A date in January 1970 means a current seconds value was read as milliseconds.
1700000000becomes1970-01-20T16:13:20.000Z; the sibling literal1000000000becomes1970-01-12T13:46:40.000Z. Anything in the first four weeks of 1970 is this and only this. - A five- or six-digit year means a millisecond value was read as seconds.
1700000000000becomes+055840-11-08T22:13:20.000Z— ISO 8601 requires the leading+for years outside 0000–9999, so the plus sign itself is the tell. - "Invalid Date" or a thrown error often means a microsecond value was read as
seconds.
1700000000000000is not a representable date — 1.7×1018 milliseconds is far outside the ±8.64×1015 range a JavaScriptDateholds.
Worked example: the leap-day Kafka record
A consumer logs a record timestamp of 1709164800000 and the alert says the message is
from the future. Thirteen digits, so milliseconds, so the instant is
2024-02-29T00:00:00.000Z — midnight UTC on 29 February 2024, a real leap day.
Nothing is from the future. What made it look wrong was a downstream service dividing by 1000 with
integer maths and then formatting in a local zone, which shifted the date across midnight and landed
it on 28 February — a day that, in a leap year, invites exactly this kind of second-guessing.
The diagnostic sequence is worth internalising because it takes seconds: count the digits, decode the raw value here, compare against what the failing system displayed. If Epochly agrees with the producer, the consumer is broken. If Epochly agrees with the consumer, the producer wrote the wrong unit and every stored row is affected, not just the one you noticed.
Converting milliseconds in code, safely
The scaling itself is trivial; the discipline is in never letting a bare integer cross a boundary without its unit attached. Three habits that eliminate most of these bugs:
- Put the unit in the name.
updated_at_msandupdated_at_secondscost nothing and make the mistake visible in code review, where it is free to fix. A column calledtimestamptells the next person nothing. - Prefer ISO 8601 strings at API boundaries.
2023-11-14T22:13:20.000Zcannot be misread as either unit, sorts correctly as text, and costs a few bytes. Keep epoch integers for storage and internal maths where the unit is enforced by a schema. - Assert the digit count on ingest. A three-line guard that rejects a 10-digit value in a field documented as milliseconds catches the whole class of bug at the edge, before it reaches a database and becomes a migration.
Related conversions
- Unix timestamp to date — the general decode page, with the full digit-length table behind auto-detection.
- Date to Unix timestamp — the reverse direction, and the eleven-zone table showing what "the same time" costs.
- Current Unix timestamp — the live count in both seconds and milliseconds, side by side.
- Discord timestamp generator — Discord wants seconds, so this is where a 13-digit value has to be divided.