iKit
Technical · 9 min read ·

Why Your JavaScript Timestamp Is 1000× Your Backend's (2026)

Your JavaScript timestamp is in milliseconds while most backends use seconds, so it looks 1000× too big. Here is why, and how to convert it safely.

Why Your JavaScript Timestamp Is 1000× Your Backend's (2026)

Why Your JavaScript Timestamp Is 1000× Your Backend's

You log a timestamp in Node, compare it to a value from your database or a JWT, and one number is a thousand times bigger than the other. Nothing is broken: JavaScript measures time in milliseconds since the Unix epoch, while most backends measure in seconds. This guide explains why a JavaScript timestamp is 1000× your backend's, how to convert between the two without bugs, and where the mismatch bites hardest.

TL;DR

  • JavaScript timestamps are milliseconds since the epoch; Unix time is seconds.
  • One second is 1000 ms, so the JS value is 1000× larger.
  • Seconds have ~10 digits today; milliseconds have ~13.
  • Convert with Math.floor(Date.now() / 1000) and new Date(sec * 1000).
  • JWT exp/iat, SQL, and cron all expect seconds.

Why is a JavaScript timestamp 1000× bigger than a Unix timestamp?

Both numbers count from the same instant, but in different units. That single difference is behind most "wrong date" bugs in web apps, and once you have seen it you will start to spot it everywhere — in logs, in API payloads, in failing auth checks.

Milliseconds vs seconds since the epoch

In JavaScript, Date.now() returns the number of milliseconds that have elapsed since the Unix epoch, per MDN. So does new Date().getTime(), and so does the value you get when you coerce a Date to a number with +date or date * 1. A "Unix timestamp" almost everywhere else — time() in PHP and C, date +%s in the shell, EXTRACT(EPOCH FROM ...) in PostgreSQL — is in seconds. Since one second equals 1000 milliseconds, the JavaScript number is exactly 1000× the seconds value for the same moment. The choice is deliberate, not a quirk: JavaScript needs sub-second precision for animation, timeouts, and performance measurement, so the language standardised on milliseconds as its base time unit long ago, and every Date method follows suit.

How to tell which one you're holding (10 vs 13 digits)

The fastest check is digit count. A seconds-based timestamp for any current date is about 1.7 billion — ten digits. The millisecond version is about 1.7 trillion — thirteen digits.

Unit Example (same instant) Digits
Seconds 1748649600 10
Milliseconds 1748649600000 13

If a value is roughly 1000× larger or smaller than you expected, you almost certainly have a unit mismatch, not a timezone or off-by-one error. This heuristic stays reliable for a very long time: seconds timestamps do not gain an eleventh digit until the year 2286, and millisecond timestamps keep their thirteen digits well past any code you will ship this decade. So "ten digits means seconds, thirteen means milliseconds" is a safe rule of thumb in practice.

The shared starting point: the Unix epoch

Both counts start at exactly midnight UTC on 1 January 1970 — the Unix epoch. Nothing about JavaScript's choice is wrong; it simply defines its internal time value as milliseconds rather than seconds. The conflict only appears when a millisecond value meets a system that assumed seconds, or vice versa. Because the origin is identical on both sides, conversion never involves timezones, leap seconds, or calendar math — it is pure arithmetic on a single integer, which is exactly why the fix is so simple once you spot the cause.

How to convert a JavaScript timestamp to seconds

Conversion is just multiply or divide by 1000. The only traps are doing it in the right direction and remembering to floor the result.

Math.floor(Date.now() / 1000)

To produce a classic seconds timestamp from JavaScript, divide by 1000 and drop the fractional part:

// milliseconds -> seconds
const sec = Math.floor(Date.now() / 1000);

Use Math.floor, not Math.round: rounding can push you a full second into the future, which quietly breaks "not before" windows and rate-limit checks that compare against the current second. Flooring matches what time() in PHP and date +%s in the shell do on the backend, so both sides agree down to the exact second instead of drifting apart by one.

Converting seconds back to a JS Date

Going the other way, multiply the seconds value by 1000 before handing it to Date, because the Date constructor reads a lone number as milliseconds:

const seconds = 1748649600;
const d = new Date(seconds * 1000);
console.log(d.toISOString());
// 2025-05-31T00:00:00.000Z

Once you have a real Date object, format it however the context needs: toISOString() for storage, logs, and API responses, or toLocaleString() for something a person will read on screen.

Why new Date(seconds) gives you 1970

Forget the * 1000 and you hit the most common version of this bug: new Date(1748649600) is interpreted as 1,748,649,600 milliseconds after the epoch — about 20 days — so it lands in late January 1970. The value was fine; the unit was wrong. Whenever a date renders as a morning in January 1970, suspect a seconds value passed where milliseconds were expected. You can confirm what a raw number actually represents by pasting it into a Unix timestamp converter, which shows both the seconds and milliseconds interpretation side by side so the correct reading is obvious at a glance.

Where the mismatch bites in real code

The bug is rarely in your own arithmetic — it lives at the seams between systems that disagree on the unit. These are the three places it shows up most often.

JWT exp and iat are in seconds

JSON Web Token claims like exp (expiration) and iat (issued-at) are NumericDate values measured in seconds, not milliseconds. Comparing them to Date.now() directly will always make a valid token look like it expires fifty thousand years from now:

const { exp } = payload;          // seconds
const nowSec = Date.now() / 1000; // convert first!
const expired = nowSec > exp;

When you inspect a token in a JWT decoder, remember those claim numbers are seconds before you reason about whether the token has expired or is not yet valid.

Database and API timestamps

PostgreSQL EXTRACT(EPOCH FROM ...), MySQL UNIX_TIMESTAMP(), and most REST APIs emit seconds. Some platforms standardise explicitly — Stripe, for instance, uses seconds across its entire API — while a handful of JavaScript-first services hand back milliseconds. The safe move is to never assume: check the units the moment a payload arrives, especially when the value is buried several levels deep in nested JSON where it is easy to miss. A quick pass through a JSON formatter lays the structure out cleanly and makes a 10- versus 13-digit field jump straight out.

Logging and cron expressions

Shell and systems tooling lives in seconds. date +%s prints seconds, and date -d @1748649600 reads a seconds value back into a human-readable date. Python's time.time() returns seconds since the epoch as a float, per the Python documentation. If you ship a millisecond value into any of these, you get a timestamp tens of thousands of years in the future — and the frustrating part is that it usually does not throw, it just writes nonsense into your logs that you only notice much later.

How to stop the bug from happening again

A handful of small habits eliminate this whole class of error before it ever reaches production.

Name variables with the unit

Make the unit part of the name so a mismatch is visible right at the call site instead of three stack frames away:

  • tsMs / tsSec instead of a bare ts
  • expiresAtSec for anything compared against a JWT claim
  • createdAtMs for values that came straight from Date.now()

A reviewer catches foo(expiresAtSec, createdAtMs) instantly; nobody catches foo(a, b). The unit suffix is the cheapest documentation you can write.

Convert at the boundary

Pick one internal unit — milliseconds is the natural choice in a JavaScript codebase — and convert immediately at every edge: the moment data enters from an API or database, and the moment it leaves to one. Internal code then never has to guess, and when something is off you have exactly one layer to inspect rather than a tangle of ad-hoc conversions scattered through the call stack.

Sanity-check the digit count

A cheap runtime guard catches accidental swaps before a user ever sees them:

function assertSeconds(ts) {
  // ~10 digits today; reject 13-digit ms
  if (ts > 1e12) throw new Error("looks like ms");
  return ts;
}

Drop this at the boundary where you pass a value to anything that expects seconds, and a misrouted millisecond timestamp fails loudly in development instead of silently in production months later.

References

Related on iKit

Related posts