Skip to content

Date & Time

Why Your Unix Timestamp Is 1000 Times Off

Ten digits is seconds, thirteen is milliseconds. A date in 1970 means you mixed them one way; a date in the year 56000 means you mixed them the other.

3 min read

Your timestamp decodes to 1970, or to the year 56000. Both are the same bug: something counted seconds and something else counted milliseconds.

The two conventions

A Unix timestamp is the number of time units since 1 January 1970, 00:00:00 UTC. The original definition uses seconds. JavaScript uses milliseconds. Both are called “Unix timestamps” constantly.

Unit A moment in late 2026 looks like Digits Used by
Seconds 1788000000 10 Unix, PHP time(), Python, Go, JWT claims, most APIs
Milliseconds 1788000000000 13 Date.now(), Java, Kotlin, most JSON from browsers

Count the digits. Ten digits is seconds. Thirteen is milliseconds. That single check catches almost every instance of this, and it stays reliable until the year 2286.

What each mistake looks like

The symptom tells you which direction you went.

  • You get 1970, or a date in January of that year. You passed milliseconds to something expecting seconds. Dividing by 1000 was skipped, so a real date collapsed to a moment a few days after the epoch.
  • You get a date tens of thousands of years away. You passed seconds to something expecting milliseconds. The number got multiplied by 1000 in effect, and the date ran off the end of the calendar.
  • Everything is 1000× too short. A duration calculated in one unit and formatted in the other. A three-hour job reports as eleven seconds.

Paste the number into our Epoch & Unix Timestamp Converter and it tells you which unit it is before converting, so you find out you had the wrong one rather than getting a confidently wrong date.

Where it bites hardest: token expiry

The exp, iat and nbf claims in a JWT are all in seconds. That’s fixed by the spec. Meanwhile the natural way to produce “now” in JavaScript is Date.now(), which is milliseconds.

Write exp: Date.now() + 3600000 and you’ve created a token that expires in the year 58,000. It will never be rejected. That’s not a small bug — it’s an authentication token with no working expiry, and nothing about it looks wrong until someone reads the number.

The correct form is Math.floor(Date.now() / 1000) + 3600. If you’re debugging a token that behaves oddly, the JWT Decoder renders the time claims as real dates, which makes a year-58000 expiry immediately obvious. There’s more on what else is in there in what’s actually inside a JWT.

Converting, in the languages you’ll meet

Language Seconds now Milliseconds now
JavaScript Math.floor(Date.now() / 1000) Date.now()
PHP time() (int) (microtime(true) * 1000)
Python int(time.time()) int(time.time() * 1000)
SQL (MySQL) UNIX_TIMESTAMP() UNIX_TIMESTAMP() * 1000

Use Math.floor, not Math.round. Rounding can put your timestamp up to half a second in the future, which is enough for a strict nbf check to reject a token you just issued.

Two things that are not the same bug

Timezones. A Unix timestamp has no timezone. It’s an absolute count from a fixed instant in UTC. If your date is out by a whole number of hours, that’s a formatting problem at the display end, not a units problem. Different question — see the Time Zone Converter.

2038. A signed 32-bit integer holding seconds overflows on 19 January 2038. That’s a storage width problem, not a units problem, and it’s a real deadline for anything still using a 32-bit time type. Milliseconds in a 64-bit integer are fine for longer than the question is interesting.

Stop it happening again

  • Put the unit in the name. expiresAtSec and createdAtMs cost nothing and end the ambiguity permanently.
  • Convert once, at the edge of your system, and use one unit internally.
  • Sanity-check on read: a timestamp under 1010 is seconds, above it is milliseconds. Three lines, and it fails loudly instead of storing a date in the year 56000.

The short version

Ten digits is seconds, thirteen is milliseconds. 1970 means you passed milliseconds where seconds were wanted; a far-future date means the reverse. JWT time claims are always seconds, and Date.now() is always milliseconds.

Tools in this guide

All of them run in your browser. Nothing uploaded.

Last updated: September 4, 2026