Unix Timestamp Seconds vs Milliseconds: The 1000x Bug

A Unix timestamp in seconds is a 10-digit number counting seconds since 1970-01-01 UTC; in milliseconds it is 13 digits. JavaScript's Date uses milliseconds, while most other languages use seconds, so mixing them multiplies or divides your value by 1000 and throws the date tens of thousands of years off.

What the 1000x bug actually looks like

The bug is an off-by-1000x error, not an off-by-one. It happens when a value measured in one unit is handed to code that assumes the other. Because the scaling factor is 1000, the result is never subtly wrong; it is catastrophically wrong, which is the one bit of good news. You will not ship a date that is a day off. You will ship a date in the wrong millennium.

Concretely, take a normal seconds timestamp for late 2024, 1729345200. Pass it straight into new Date(), which expects milliseconds, and JavaScript reads it as 1,729,345,200 ms after the epoch: roughly 20 days into January 1970. The reverse is more dramatic. Take a 13-digit millisecond value, 1729345200000, and feed it to a function that expects seconds (Python's datetime.fromtimestamp, say). It now treats 1.7 trillion as a count of seconds, which lands around the year 56,700 AD, more than 50,000 years in the future.

So the heuristic is simple. If a date renders in early 1970 when it should be recent, you divided when you should not have, or you forgot to multiply. If it renders 50,000 years from now, you multiplied or you handed milliseconds to a seconds-based API.

How to tell seconds from milliseconds

The fastest field test is counting digits, because the two formats do not overlap for any date you care about.

UnitDigits (modern dates)ExampleRenders as
Seconds1017293452002024-10-19 UTC
Milliseconds1317293452000002024-10-19 UTC
Microseconds161729345200000000databases, tracing
Nanoseconds191729345200000000000Go, event pipelines

Ten-digit values stay in the seconds range from September 2001 (when timestamps first crossed 1,000,000,000) until November 2286. Thirteen-digit millisecond values cover roughly the same era. Because there is a three-digit gap, a quick length check almost always resolves the ambiguity. A defensive programmatic version uses a threshold rather than string length:

function toMillis(ts) {
  // Values below ~1e12 are almost certainly seconds for modern dates.
  return ts < 1e12 ? ts * 1000 : ts;
}

This is a heuristic, not a law. It breaks for dates before September 2001 (seconds timestamps there are also under 1e12) and for the year-1970 edge. For input you control, prefer an explicit unit over guessing. For input you do not control, the threshold plus a sanity check on the resulting year is a reasonable guard.

JavaScript wants milliseconds: multiply by 1000

JavaScript's Date is the odd one out. Date.now(), getTime(), and the new Date(number) constructor all operate in milliseconds since the epoch. This dates back to 1995, when Date was modeled on Java's java.util.Date, which already used milliseconds. Most of the rest of the ecosystem (POSIX time(), Python, PHP, Ruby, Go's Unix(), SQL epoch functions) defaults to seconds. That mismatch at the boundary is where the bug lives.

So when you receive a seconds timestamp from an API or database and want a JavaScript Date, multiply by 1000:

const seconds = 1729345200;          // from a Python/PHP backend
const date = new Date(seconds * 1000); // correct: 2024-10-19
// new Date(seconds)                  // WRONG: Jan 1970

// already milliseconds? do NOT multiply
const ms = 1729345200000;
const date2 = new Date(ms);          // correct as-is

To go the other direction and produce a seconds timestamp from JavaScript for a backend that expects them, divide by 1000 and floor:

const nowSeconds = Math.floor(Date.now() / 1000);     // current time, seconds
const fromDate = Math.floor(someDate.getTime() / 1000); // any Date, seconds

If you would rather not eyeball any of this, paste the value into the timestamp converter; it auto-detects seconds versus milliseconds and shows you the resulting UTC and local date so a wrong unit is obvious at a glance.

Why Math.floor, not Math.round

Date.now() / 1000 produces a fractional number of seconds, for example 1729345200.873. You need to drop the fractional part to get an integer second. Use Math.floor, which truncates toward the past, rather than Math.round, which can round up.

The reason is consistency with how the rest of the world defines a Unix second. POSIX time(), the kernel, and every seconds-based language return the floored second: the count of whole seconds that have fully elapsed. If you use Math.round, a timestamp at ...200.873 rounds to ...201, putting your value up to half a second ahead of the same instant computed in C, Python, or the database. That breaks equality checks, causes off-by-one-second comparisons, and can make a token's iat appear to be issued in the future relative to a server using floor. Math.floor matches everyone else, so use it.

Store timezone-neutral: prefer ISO 8601 or UTC seconds

The most durable fix is to stop juggling units at the boundary by picking one canonical representation and enforcing it everywhere. Two good choices, both timezone-neutral:

  • ISO 8601 strings in UTC with a trailing Z, like 2024-10-19T12:00:00Z. Self-describing, human-readable in logs, unambiguous about the unit, and parsed natively by new Date("...") and almost every other language.
  • Integer Unix seconds in UTC. Compact, sorts numerically, and the universal seconds convention. Just document the unit in your schema so nobody re-introduces the 1000x bug.

Whichever you choose, keep everything in UTC at rest and only convert to a local timezone at the moment of display. A Unix timestamp, in either unit, is inherently timezone-neutral because it counts from a fixed UTC instant; the timezone confusion enters only when you format it. Storing local times, or worse, naive datetimes with no zone, reintroduces the ambiguity you were trying to escape, especially across daylight-saving transitions. If you are debugging a token whose exp and iat are raw seconds, the JWT decoder renders those claims as readable dates so you can confirm the unit and the timezone at a glance.

A final sanity check that catches most unit mistakes for free: after converting, look at the year. If it is in 1970 or it is 50,000 years from now, you have the classic 1000x bug, and you are off by exactly one multiply or divide.

Frequently Asked Questions

By default, a Unix timestamp is the number of seconds since 1970-01-01 UTC, which is a 10-digit number for modern dates. JavaScript is the main exception: its Date object works in milliseconds, which is 13 digits.

Multiply the seconds value by 1000, because the Date constructor expects milliseconds. For example, new Date(1729345200 * 1000) gives the correct 2024 date, while passing the seconds value directly lands in January 1970.

Date.now() returns milliseconds, so dividing by 1000 yields fractional seconds. Math.floor truncates to whole elapsed seconds, matching POSIX time() and every other language; Math.round can push the value a fraction ahead and cause off-by-one-second mismatches.

Count the digits: 10 digits is seconds and 13 digits is milliseconds for any recent date. Programmatically, a value below about 1e12 is almost certainly seconds; you can also convert it and check whether the resulting year is reasonable.