← All posts

Milliseconds vs Seconds in Unix Timestamps

Modern Unix seconds are usually 10 digits, while Unix milliseconds are usually 13 digits. Divide milliseconds by 1000 for seconds, multiply seconds by 1000 for milliseconds, and treat dates in 1970 or the year 55,000 as unit mismatch warnings.

Quick answer

Milliseconds vs seconds is a unit problem, not a timezone problem.

What you have Modern digit count Example Same instant in UTC Common systems
Unix seconds 10 digits 1700000000 2023-11-14T22:13:20Z Linux date +%s, Python time.time(), PHP time(), JWT exp
Unix milliseconds 13 digits 1700000000000 2023-11-14T22:13:20Z JavaScript Date.now(), Java currentTimeMillis(), MongoDB Date, .NET milliseconds
Unix microseconds 16 digits 1700000000000000 2023-11-14T22:13:20Z some databases, event streams, high-precision logs
Unix nanoseconds 19 digits 1700000000000000000 2023-11-14T22:13:20Z Go UnixNano(), tracing systems, low-level clocks

The conversion is simple:

const seconds = Math.floor(milliseconds / 1000);
const backToMs = seconds * 1000;

The debugging rule is even simpler:

Symptom Likely mistake Fix
date lands in January 1970 seconds were used where milliseconds were expected multiply by 1000
date lands around year 55,000 milliseconds were used where seconds were expected divide by 1000
JWT expires immediately or in decades Date.now() was copied into a seconds claim use Math.floor(Date.now() / 1000)
cache TTL is 1000x too long or short duration milliseconds were confused with timestamp seconds separate ttlSeconds from expiresAtMs

Why Unix timestamps come in both units

Classic Unix time is seconds since 1970-01-01 00:00:00 UTC. That is why shell tools, PHP, Python's basic time.time(), JWT claims, and many server APIs speak seconds by default.

JavaScript chose milliseconds for Date, and that convention spread into browser events, frontend logs, Java's System.currentTimeMillis(), MongoDB Date, and many JSON APIs written around JavaScript clients.

Neither unit is more "real." They describe the same instant at different precision:

1700000000 seconds      = 2023-11-14T22:13:20Z
1700000000000 millis    = 2023-11-14T22:13:20Z
1700000000000000 micros = 2023-11-14T22:13:20Z

The mistake happens at boundaries: browser to API, Java to SQL, webhook to queue, CSV import to database, or cache expiry to Redis.

The 10-digit vs 13-digit rule

For positive timestamps around today, digit count is the fastest check.

Digits Unit Approximate current scale Notes
10 seconds 1.7e9 seconds stay 10 digits from 2001-09-09 to 2286-11-20
13 milliseconds 1.7e12 milliseconds are 13 digits over the same modern date range
16 microseconds 1.7e15 divide by 1_000_000 for seconds
19 nanoseconds 1.7e18 too large for exact JavaScript Number arithmetic

Use digit count as a guardrail, not as a permanent API contract.

function toEpochMs(value) {
  const n = Number(value);
  if (!Number.isFinite(n)) throw new TypeError("timestamp must be numeric");
  if (n < 0) throw new RangeError("negative timestamps need an explicit unit");

  // Good enough for positive contemporary seconds or milliseconds.
  return n < 100_000_000_000 ? n * 1000 : n;
}

That helper is intentionally conservative. It is fine for messy analytics imports and webhook cleanup jobs. It is not fine for historical datasets, pre-1970 dates, astronomy, archival systems, or anything where the upstream should have documented the unit.

Convert milliseconds to seconds

Most "milliseconds to Unix timestamp" searches mean this: "I have a 13-digit JavaScript or Java value, and the target wants 10-digit Unix seconds."

const createdAtMs = Date.now();
const createdAtUnixSeconds = Math.floor(createdAtMs / 1000);

Use integer output for API fields, database columns, JWT claims, and shell commands.

Language Milliseconds to seconds
JavaScript Math.floor(ms / 1000)
Python ms // 1000
Java ms / 1000L for modern positive values, Math.floorDiv(ms, 1000L) if negatives matter
Go ms / 1000 with int64
C# ms / 1000L
PHP intdiv($ms, 1000)
Shell $((ms / 1000))
PostgreSQL floor(ms / 1000.0)::bigint

One subtlety: negative timestamps behave differently across languages. JavaScript Math.floor(-1500 / 1000) returns -2, while Java integer division -1500 / 1000L returns -1. If your data can be before 1970, choose the rounding rule on purpose and test it.

Convert seconds to milliseconds

Use this when a seconds-based API value needs to become a JavaScript Date, Java Instant, or millisecond database field.

const createdAtSeconds = 1700000000;
const createdAtMs = createdAtSeconds * 1000;
const createdAtDate = new Date(createdAtMs);
Language Seconds to milliseconds
JavaScript seconds * 1000
Python seconds * 1000
Java seconds * 1000L
Go seconds * 1000 with int64
C# seconds * 1000L
PHP $seconds * 1000
SQL seconds * 1000::bigint or equivalent

Use 64-bit numbers. Current Unix seconds fit in 32 bits for now, but current Unix milliseconds already do not. This bug is easy to miss in Java, C#, SQL schemas, Protobuf messages, and older code that still uses int.

int seconds = 1700000000;
long wrong = seconds * 1000;   // overflow happens before assignment
long right = seconds * 1000L;  // long arithmetic

Which unit each system expects

This table is the part to keep open during code review.

System or API Common timestamp unit Example
JavaScript Date.now() milliseconds 1700000000000
JavaScript new Date(number) milliseconds new Date(1700000000 * 1000)
Java System.currentTimeMillis() milliseconds 1700000000000
Java Instant.getEpochSecond() seconds 1700000000
.NET ToUnixTimeSeconds() seconds 1700000000
.NET ToUnixTimeMilliseconds() milliseconds 1700000000000
Python time.time() seconds as float 1700000000.123
Python time.time_ns() nanoseconds 1700000000123000000
Go time.Now().Unix() seconds 1700000000
Go time.Now().UnixMilli() milliseconds 1700000000000
Go time.Now().UnixNano() nanoseconds 1700000000000000000
PHP time() seconds 1700000000
Ruby Time.now.to_i seconds 1700000000
Linux date +%s seconds 1700000000
JWT exp, iat, nbf seconds 1700000000
Stripe-style API timestamp fields seconds created: 1700000000
MongoDB BSON Date milliseconds stored as 64-bit milliseconds

Fix the 1970 bug

This is the JavaScript version:

new Date(1700000000).toISOString();
// "1970-01-20T16:13:20.000Z"  wrong

new Date(1700000000 * 1000).toISOString();
// "2023-11-14T22:13:20.000Z"  right

It happens because new Date(number) expects milliseconds. A 10-digit seconds value is only about 1.7 billion milliseconds after 1970, which is still January 1970.

The same issue appears in frontend charts, browser logs, React state, date pickers, and analytics dashboards. If the UI shows 1970, look for a seconds value that skipped the * 1000 boundary conversion.

Fix the year-55000 bug

This is the reverse problem:

from datetime import datetime, timezone

datetime.fromtimestamp(1700000000000, tz=timezone.utc)
# ValueError or a far-future year on many systems

datetime.fromtimestamp(1700000000000 / 1000, tz=timezone.utc)
# 2023-11-14 22:13:20+00:00

Python fromtimestamp, Linux date -d @SECONDS, PHP date, many SQL epoch functions, and many API filters expect seconds. A 13-digit millisecond value becomes about 54,000 years when read as seconds.

When you see a far-future date, do not debug timezone first. Check the unit first.

JWT, tokens, and expiry fields

JWT exp, iat, and nbf use NumericDate, which is seconds since 1970-01-01T00:00:00Z UTC. This is a common production bug because JavaScript developers naturally reach for Date.now().

// Wrong: milliseconds
const exp = Date.now() + 60 * 60 * 1000;

// Right: seconds
const exp = Math.floor(Date.now() / 1000) + 60 * 60;

The same thinking applies to API fields named expires_at, created, issued_at, and not_before. If the docs show a 10-digit value, treat it as seconds unless they explicitly say otherwise.

Database and schema naming

The best long-term fix is boring names.

Use:

  • created_at_seconds
  • created_at_ms
  • expires_at_unix_seconds
  • client_sent_at_ms
  • event_time_ns

Avoid:

  • timestamp
  • time
  • created
  • date
  • expires

If the database column is numeric, put the unit in the column name and use a 64-bit type:

created_at_seconds BIGINT NOT NULL,
client_sent_at_ms BIGINT NOT NULL

If humans need to query ranges often, consider storing a native timestamp type as well:

created_at TIMESTAMPTZ NOT NULL,
created_at_ms BIGINT NOT NULL

That gives you readable SQL and a compact protocol value. The important part is that each column has one job.

Safe boundary patterns

Pick one unit inside a subsystem and convert only at the edge.

// API returns seconds. Frontend stores milliseconds.
type UserApi = {
  id: string;
  created_at: number; // Unix seconds from API
};

type UserView = {
  id: string;
  createdAtMs: number;
};

function fromApi(user: UserApi): UserView {
  return {
    id: user.id,
    createdAtMs: user.created_at * 1000,
  };
}

If you use TypeScript, branded types can make accidental mixing harder:

type UnixSeconds = number & { readonly unit: "seconds" };
type UnixMs = number & { readonly unit: "milliseconds" };

function secondsToMs(seconds: UnixSeconds): UnixMs {
  return (seconds * 1000) as UnixMs;
}

That is not magic security. It is just friction in the right place.

Microseconds, nanoseconds, and JavaScript precision

Each unit step is a factor of 1000:

From To seconds To milliseconds
milliseconds ms / 1000 already milliseconds
microseconds us / 1_000_000 us / 1000
nanoseconds ns / 1_000_000_000 ns / 1_000_000

The JavaScript precision trap starts at nanoseconds. A current Unix nanosecond timestamp is around 1.7e18, which is larger than Number.MAX_SAFE_INTEGER. Keep nanoseconds as BigInt or strings if exactness matters.

const ns = 1700000000000000000n;
const ms = ns / 1000000n;

If the value is only for display, converting down to milliseconds is usually fine. If it is for ordering distributed traces, audit logs, or financial events, do not silently round away precision.

A review checklist for timestamp PRs

Before merging code that moves time across a boundary, check these:

  • Does the variable or field name include the unit?
  • Does the receiving API document seconds, milliseconds, microseconds, or nanoseconds?
  • Is a 13-digit value ever sent to a seconds-only field?
  • Is a 10-digit value ever passed directly to JavaScript Date?
  • Are JWT exp, iat, and nbf values seconds?
  • Are database columns 64-bit if they store milliseconds?
  • Are pre-1970 values possible, and if so, is rounding tested?
  • Are nanoseconds kept out of JavaScript Number when exactness matters?

The fastest one-line code review question is: "What unit is this timestamp at this boundary?"

Official references

Related timestamp guides

FAQ

Is a 13-digit timestamp seconds or milliseconds?
For contemporary Unix timestamps, 13 digits means milliseconds. A current seconds value is around 1.7 billion and has 10 digits; the same instant in milliseconds is around 1.7 trillion and has 13 digits.
How do I convert milliseconds to seconds?
Divide by 1000. For JavaScript API payloads that need integer Unix seconds, use Math.floor(ms / 1000). In Python, use ms // 1000. In Java, use Math.floorDiv(ms, 1000L) if pre-1970 values are possible; otherwise ms / 1000L is fine for modern positive timestamps.
How do I convert seconds to milliseconds?
Multiply by 1000. Use a 64-bit type in Java, C#, Go, SQL, and Protobuf because current Unix milliseconds do not fit in a signed 32-bit integer.
Why does my date show 1970?
A 10-digit Unix seconds value was passed to something that expects milliseconds, such as new Date(1700000000) in JavaScript. Multiply by 1000 before creating the Date.
Why does my date show the year 55,000?
A 13-digit millisecond value was passed to something that expects seconds. Divide by 1000 before using Python datetime.fromtimestamp, Linux date -d @SECONDS, PHP date, or a seconds-based API field.
Does JavaScript Date.now() return seconds or milliseconds?
Milliseconds. Date.now() returns milliseconds since 1970-01-01 00:00:00 UTC. Use Math.floor(Date.now() / 1000) when an API expects Unix seconds.
Are JWT exp and iat seconds or milliseconds?
Seconds. RFC 7519 defines JWT NumericDate as seconds since 1970-01-01T00:00:00Z UTC, ignoring leap seconds. Do not put Date.now() directly into exp, iat, or nbf; divide by 1000 first.
Should I store timestamps in seconds or milliseconds?
Store whichever unit your system contract needs, but name it explicitly: created_at_seconds, createdAtMs, expiresAtUnixSeconds, or event_time_ms. If you control both sides and need JavaScript-friendly values, milliseconds are convenient. If you interoperate with Unix tools, JWTs, or many server APIs, seconds are often easier.
Can I auto-detect seconds vs milliseconds?
You can for positive contemporary values, such as treating values below 100000000000 as seconds and larger values as milliseconds. Do not rely on that for negative timestamps, historical data, far-future dates, or mixed-precision datasets.
Are microseconds and nanoseconds also Unix timestamps?
They can be. They use the same epoch but a smaller unit: 16 digits is usually microseconds and 19 digits is usually nanoseconds for current dates. JavaScript Number cannot safely hold current Unix nanoseconds exactly, so use BigInt or keep them as strings.