← All posts

Milliseconds vs Seconds in Unix Timestamps

Use digit count first: modern Unix seconds are 10 digits, modern Unix milliseconds are 13 digits. Divide milliseconds by 1000 to get Unix seconds, multiply seconds by 1000 to get milliseconds, and suspect a unit bug when dates land in 1970 or the year 55,000.

Why there are two units

Unix originally defined timestamps as seconds since 1970-01-01 UTC — that's how POSIX standardizes time_t. Then JavaScript landed in 1995 with a Date constructor that took milliseconds. Java followed with System.currentTimeMillis(). MongoDB BSON Date is 64-bit milliseconds. .NET added ToUnixTimeMilliseconds. The split is now permanent: most server languages and Unix tools use seconds; most client-side and JVM runtimes use milliseconds; sub-second-precision databases use microseconds or nanoseconds. The single best habit is naming fields with the unit so the next reader doesn't have to guess.

  • Seconds (POSIX standard): Python, PHP, Go, Ruby, Shell, most APIs
  • Milliseconds (JavaScript convention): JS Date, Java currentTimeMillis, MongoDB BSON Date, Java Instant.toEpochMilli, .NET DateTimeOffset.ToUnixTimeMilliseconds
  • Microseconds (database-precision): PostgreSQL TIMESTAMPTZ, BigQuery TIMESTAMP_MICROS, ClickHouse DateTime64
  • Nanoseconds (high-precision): Go time.UnixNano, Linux clock_gettime, InfluxDB, OpenTelemetry, Java Instant
  • Picosecond and finer: research instrumentation only; no mainstream API

The 10-digit vs 13-digit rule

The fastest way to tell whether a number is Unix seconds or milliseconds is digit count. In 2026, current Unix seconds are 10 digits (around 1.7×10^9), current Unix milliseconds are 13 digits (around 1.7×10^12), microseconds are 16 digits, nanoseconds are 19 digits. The boundaries shift slowly — a 10-digit value will represent seconds until the year 2286 — but for any timestamp written in the last 25 years, the rule is reliable. Use it as a runtime guard when ingesting timestamps from upstream systems whose contract isn't strict.

  • 10 digits → Unix seconds (1.7×10^9 = ~2026; range 2001 to 2286)
  • 13 digits → Unix milliseconds (1.7×10^12 = ~2026; range 2001 to 2286)
  • 16 digits → Unix microseconds (1.7×10^15 = ~2026)
  • 19 digits → Unix nanoseconds (1.7×10^18 = ~2026)
  • Auto-detect runtime check: const isMs = ts > 1e11 // ms threshold is ~year 5138
  • Or normalize on read: const normalizeToMs = ts => ts < 1e11 ? ts * 1000 : ts

10-digit timestamp

For modern dates, 10 digits means Unix seconds. Pass it to Python fromtimestamp, PHP time-style functions, Linux date -d @, or multiply by 1000 before using JavaScript Date.

13-digit timestamp

For modern dates, 13 digits means Unix milliseconds. Pass it directly to JavaScript Date, Java Instant.ofEpochMilli, or .NET FromUnixTimeMilliseconds.

16- and 19-digit precision

16 digits usually means microseconds; 19 digits usually means nanoseconds. Divide by 1,000,000 or 1,000,000,000 before using seconds-based tools.

Convert milliseconds to seconds — the canonical recipe

Divide by 1000 and floor. The integer-division form is preferred over plain float division because most APIs and database columns that accept Unix seconds reject or silently truncate floats. Use Math.floor() when you want to round down (the common case for storage and API contracts), Math.round() when you want to round to the nearest second (slightly more accurate for display). For values that may be negative (pre-1970 timestamps), Math.floor is symmetric — JavaScript's Math.floor(-1500 / 1000) returns -2, which is the right pre-1970 second; bare truncation gives -1, which is one second off.

  • JavaScript: Math.floor(ms / 1000) // integer Unix seconds
  • Python: ms // 1000 // integer floor division — same result
  • Java: ms / 1000L // long integer division
  • Go: ms / 1000 // int64 / int64 = int64
  • Kotlin: ms / 1000 // Long division
  • C#: ms / 1000L // long division
  • PHP: intdiv($ms, 1000) // PHP 7.0+
  • Rust: ms / 1000 // i64 division
  • Shell: $((ms / 1000)) // POSIX-safe

Milliseconds to timestamp

When docs say Unix timestamp and show a 10-digit example, they mean seconds. Divide the 13-digit millisecond value before sending it.

Floor instead of raw division

Use integer division or Math.floor so the receiving system gets a clean seconds integer rather than a float.

Convert seconds to milliseconds — the reverse direction

Multiply by 1000. Watch for integer overflow on 32-bit types — multiplying a current 10-digit seconds value by 1000 yields a 13-digit number that exceeds the signed 32-bit range. JavaScript handles this transparently because Date.now() is float64; Java needs the L suffix to force long arithmetic; Go's int64 default is safe. Always use a 64-bit numeric type when storing or transmitting milliseconds to avoid the silent overflow.

  • JavaScript: ms = seconds * 1000 // float64; no overflow concern
  • Java: ms = seconds * 1000L // L suffix forces long arithmetic
  • Go: ms := seconds * 1000 // int64 by default
  • Python: ms = seconds * 1000 // int has arbitrary precision; no overflow
  • C#: ms = seconds * 1000L // long needed for any post-2038 value
  • Common bug: 32-bit int multiplication overflow — int seconds = ...; long ms = seconds * 1000; // overflows for current dates

Which unit each language uses

Most timestamp bugs are unit drift at a system boundary. Knowing which unit each runtime defaults to is the prerequisite for catching the bug at code-review time. The table below summarizes the dominant 'now' call in each major language and what unit it returns. Always check the documentation of any third-party API — Stripe is famous for documenting every timestamp field as 'seconds since the Unix epoch' precisely because the convention isn't universal.

  • JavaScript Date.now() → milliseconds (13 digits)
  • Java System.currentTimeMillis() → milliseconds
  • Java Instant.now().getEpochSecond() → seconds
  • Kotlin (JVM) Instant.now().toEpochMilli() → milliseconds
  • C# DateTimeOffset.UtcNow.ToUnixTimeSeconds() → seconds
  • Python time.time() → seconds (as float)
  • Python int(time.time()) → seconds (as int)
  • Go time.Now().Unix() → seconds; .UnixMilli() → milliseconds
  • PHP time() → seconds; microtime(true) → float seconds with µs precision
  • Ruby Time.now.to_i → seconds; .to_f → float seconds
  • Linux shell date +%s → seconds
  • Postgres NOW() → TIMESTAMPTZ; EXTRACT(EPOCH FROM NOW())::BIGINT → seconds
  • MySQL UNIX_TIMESTAMP() → seconds
  • MongoDB BSON Date → milliseconds
  • Stripe API: all timestamp fields documented as Unix seconds

Safe conversion patterns

Two patterns prevent most unit-drift bugs in production. First: convert at the system boundary, then never speak the other unit inside that subsystem. Second: name every variable, field, and column with the unit (createdAtMs vs createdAtSeconds), so a future reader doesn't have to count digits or read documentation. The auto-detect helper is a third pattern for ingest code that has to accept either form from a loose upstream contract — but it's a workaround, not a fix; long-term, push back on the upstream to commit to one unit.

  • Boundary conversion: const ms = response.created_at * 1000; // upstream is seconds, internal is ms
  • Field naming: createdAtMs, expiresAtSeconds, lastSeenAtNanos — no ambiguity
  • Auto-detect ingest: const toMs = ts => ts < 1e11 ? ts * 1000 : ts
  • Type-tagged wrapper (TypeScript): type UnixMs = number & { __unit: 'ms' }
  • Lint rule: forbid Date.now() / 1000 without an explicit Math.floor or Math.round
  • Code review: any timestamp-touching PR should mention the unit in its description

How unit bugs show up in production

Unit-drift bugs have a small set of symptoms that are easy to recognize once you know what to look for. The 1970 date and the year-55,000 date are the two most common — both are immediate visual flags. The harder cases are the silent ones: timestamps that look plausible but are 1000× off in some context (TTL, rate-limit windows, cache expiry), where the only symptom is misbehavior that's hard to attribute back to the unit. The bug catalog covers all 10 production patterns in detail.

  • Date shows 1970 → seconds passed where ms expected (multiply by 1000)
  • Date shows year 55,000 → ms passed where seconds expected (divide by 1000)
  • Cache TTL never expires → ms TTL set where seconds expected (1000× too long)
  • Cache TTL expires instantly → seconds TTL set where ms expected (1000× too short)
  • Rate limit window allows 1000× requests → seconds window where ms expected
  • Token expires 1000× too soon or late → unit mismatch in JWT exp claim
  • Database column overflow on insert → ms written to seconds column with bounded range

1970 bug

A 1970 date is the signature of seconds being interpreted as milliseconds, especially in JavaScript.

Year-55000 bug

A date tens of thousands of years in the future is the signature of milliseconds being interpreted as seconds.

API and database naming conventions

The cleanest way to prevent unit drift in any long-lived system is naming convention. Every field, column, log key, and protocol message that carries a Unix timestamp should include the unit in its name. createdAtMs and createdAtSeconds are unambiguous; createdAt is not. Stripe, GitHub API, and Google Cloud all document their timestamp units explicitly precisely because the cost of getting it wrong is so high. When you control the schema, follow the convention; when you don't, write a wrapper that converts to a known unit at the boundary.

  • Column name: created_at_ms (millisecond storage) or created_at_seconds (second storage)
  • JSON field: createdAtMs / createdAtSeconds — camelCase variants of the same
  • Protobuf: int64 created_at_ms = 1; // unit in the comment AND name
  • REST API: 'Returns Unix seconds since 1970-01-01' in the response schema
  • ISO 8601 alternative: created_at: '2026-06-20T09:25:15Z' — self-describing, unit-free
  • Avoid: ambiguous names like timestamp, time, created — always carry the unit

Microseconds, nanoseconds, and sub-second precision

Most application code doesn't need finer than milliseconds, but databases, distributed tracing, and high-precision systems do. PostgreSQL stores TIMESTAMPTZ as microseconds internally; BigQuery offers TIMESTAMP_MICROS and TIMESTAMP_MILLIS as separate types; ClickHouse has DateTime64 with configurable precision. Linux clock_gettime() returns nanoseconds directly; Go time.Now().UnixNano() is the canonical way to get the same value from application code. OpenTelemetry uses nanoseconds for span start/end. The conversion math is consistent: each unit step is a factor of 1000.

  • 1 second = 1,000 milliseconds = 1,000,000 microseconds = 1,000,000,000 nanoseconds
  • Microseconds (16 digits): PostgreSQL TIMESTAMPTZ, BigQuery TIMESTAMP_MICROS, Python time.time_ns() / 1000
  • Nanoseconds (19 digits): Linux clock_gettime, Go .UnixNano(), Java Instant.toEpochNanos via two longs, InfluxDB, OpenTelemetry, Prometheus exposition format
  • JavaScript sub-ms: performance.now() returns fractional ms (microsecond precision in most browsers)
  • JavaScript absolute sub-ms: performance.timeOrigin + performance.now()
  • BigInt is required for nanosecond arithmetic in JavaScript (Number loses precision past 2^53)

Unit conversion quick reference

Every Unix-time unit converts to every other via integer multiplication or division. The table below is the cheat sheet — paste into PR descriptions or wikis to settle conversion questions in one glance. The same constants work in every language: 1000 between seconds and milliseconds, 1,000,000 between seconds and microseconds, 1,000,000,000 between seconds and nanoseconds.

  • Seconds → milliseconds: × 1000
  • Milliseconds → seconds: ÷ 1000 (Math.floor for clean integer)
  • Seconds → microseconds: × 1,000,000
  • Microseconds → seconds: ÷ 1,000,000
  • Seconds → nanoseconds: × 1,000,000,000
  • Nanoseconds → seconds: ÷ 1,000,000,000
  • Milliseconds → microseconds: × 1000
  • Microseconds → milliseconds: ÷ 1000
  • Microseconds → nanoseconds: × 1000
  • Nanoseconds → microseconds: ÷ 1000

Where this guide fits in the cluster

Every other timestamp guide on this site links back here for the unit decision. Use this article when designing an API or database schema, when debugging a 1970-or-55,000 date, or when reviewing a PR that crosses a JavaScript-to-server boundary. The companion guides cover the surrounding topics — what the epoch is, how to convert in each language, the production bugs that result from getting this wrong, and the storage patterns that prevent them.

  • What the epoch is: see /blog/epoch-time-explained
  • Convert ms-specific values: see /blog/epoch-milliseconds-to-date
  • Convert epoch → date (any unit): see /blog/unix-time-to-date
  • Convert date → epoch: see /blog/date-time-to-epoch
  • Current 'now' in every language: see /blog/current-unix-timestamp
  • JavaScript-specific deep dive: see /blog/javascript-date-timestamp
  • Production bugs to avoid: see /blog/timestamp-bugs
  • Database storage: see /blog/unix-timestamp-databases

FAQ

Is a 13-digit timestamp always milliseconds?
In 2026, yes — for current dates. A 13-digit Unix value (≈1.7×10^12) is milliseconds; a 10-digit value (≈1.7×10^9) is seconds. The same instant gives different digit counts in each unit. Multiply seconds by 1000 to get milliseconds, divide milliseconds by 1000 to get seconds.
How do I identify a 10-digit vs 13-digit timestamp?
Count the digits and sanity-check the year. Current 10-digit values are Unix seconds; current 13-digit values are milliseconds. If the date decodes to 1970 or year 55,000, the unit is wrong.
Should I store seconds or milliseconds?
Match the source. JavaScript Date.now() and Java System.currentTimeMillis() emit milliseconds — store milliseconds when they're the writer. Python time.time() and most Unix tools emit seconds — store seconds in pipelines they feed. When you control both sides, document the unit in the column or field name: createdAtMs vs createdAtSeconds.
Why use Math.floor(ms / 1000) instead of ms / 1000?
Because JavaScript's / operator returns a float. Math.floor gives a clean 10-digit integer; bare / 1000 gives a float that some APIs and database columns will reject or silently truncate. Use Math.floor for the round-down behavior, Math.round to round to the nearest second.
How do I convert milliseconds to seconds?
Divide by 1000 and floor. JavaScript: Math.floor(ms / 1000). Python: ms // 1000. Java: ms / 1000L (long division). Go: ms / 1000 (int64 division). Shell: $((ms / 1000)). The integer-division form avoids any float-rounding question.
How do I convert millis to a Unix timestamp?
If 'Unix timestamp' means seconds (the common default), divide by 1000. If it means milliseconds, the millisecond value IS the Unix timestamp — both refer to the same instant, just in different units. Pick whichever the target system documents.
How do I convert currentTimeMillis to a Unix timestamp?
System.currentTimeMillis() returns Java/Kotlin Unix milliseconds. For Unix seconds: System.currentTimeMillis() / 1000L. Use the L suffix to force long arithmetic and avoid 32-bit integer overflow for any year past 2038.
Is millis the same as Unix timestamp?
It uses the same epoch — milliseconds since 1970-01-01 UTC. Classic Unix timestamp usually means seconds, so 'millis' and 'Unix timestamp' refer to the same instant in different units. Always name the variable with the unit (createdAtMs, expiresAtSeconds) to keep the next reader from guessing.
What's the Java equivalent of Date.now()?
System.currentTimeMillis() returns the same long that Date.now() returns in JavaScript — Unix milliseconds. For nanosecond precision use System.nanoTime() (but that's monotonic, not Unix time). For the modern java.time API: Instant.now().toEpochMilli().
Why does my date show 1970?
You passed Unix seconds (10 digits) to a function expecting milliseconds (13 digits). JavaScript: new Date(1700000000) lands in 1970 because the constructor reads the number as 1.7 billion milliseconds. Multiply by 1000: new Date(1700000000 * 1000).
Why does my date show the year 55,000?
The reverse case. You passed Unix milliseconds (13 digits) to a function expecting seconds. The number is interpreted as 1.7 trillion seconds — about 55,000 years past 1970. Divide by 1000 to fix: datetime.fromtimestamp(1700000000000 / 1000).