8 min read

Unix Timestamps: Seconds vs Milliseconds

Distinguish Unix timestamps in seconds and milliseconds, avoid accidental 1970 dates, and convert each unit safely across programming languages.

Many developers encounter this date bug: an event timestamp reaches a frontend and displays January 20, 1970 or causes a backend range error. The usual cause is a mismatch between seconds and milliseconds.

These errors stem from a single, pervasive point of confusion in modern software engineering: the mismatch between Unix timestamps in seconds versus milliseconds.

Both formats measure from the Unix epoch, but APIs choose different units. POSIX-oriented functions often use seconds, while JavaScript Date and Java's System.currentTimeMillis() use milliseconds. Passing one unit to an API expecting the other introduces a factor-of-1,000 error.

You can inspect, convert, and format timestamps across both scales instantly using ToolPorta's free Timestamp Converter. This guide explains how the Unix epoch works, breaks down the mathematical difference between 10-digit and 13-digit timestamps, diagnoses the root causes of common timestamp bugs, provides an automatic heuristic detection algorithm, and outlines best practices for designing time-safe APIs.

What is a Unix timestamp?#

A Unix timestamp (also known as POSIX time or Epoch time) is an integer representing the total amount of time that has elapsed since the Unix Epoch:

TEXT
1Thursday, January ,  at :: Coordinated Universal Time (UTC)

Leap seconds are ignored by convention; every day in Unix time is treated as having exactly 86,400 seconds.

Unix time is fundamentally timezone-agnostic. A timestamp represents a single, universal instant in time across the globe. Whether a user inspects a timestamp in Tokyo, Paris, London, or New York, the underlying numeric timestamp is identical. Timezones, Daylight Saving Time (DST) shifts, and calendar formatting are purely presentation-layer concerns applied when rendering the timestamp for human eyes.

The core difference: 10 digits vs 13 digits#

The fundamental division between timestamps comes down to the unit of measurement:

AttributeUnix Seconds (s)Unix Milliseconds (ms)
Unit of MeasurementSecondsMilliseconds (1/1,000 second)
Typical String Length10 digits (contemporary era)13 digits (contemporary era)
Example for 2026-10-01 00:00:00 UTC17908128001790812800000
Order of Magnitude in 2026About 1.8 billionAbout 1.8 trillion
Multiplier to ConvertMultiply by 1,000 to get msDivide by 1,000 to get seconds
PrecisionOne-second resolutionSub-second (millisecond) resolution
Dominant EcosystemsLinux, C/POSIX, Python time.time(), MySQL, PHPJavaScript/Node.js, Java, Android, MongoDB

The 10-digit scale: Unix seconds#

For over five decades, standard Unix operating systems, C libraries (time_t), and POSIX standards counted time strictly in seconds. In the contemporary 2020s, any Unix timestamp in seconds has exactly 10 decimal digits, beginning with 1 (for example, 1704067200 represents January 1, 2024 at 00:00:00 UTC).

Timestamps in seconds will remain 10 digits long until November 20, 2286, when the timestamp reaches 10000000000 (11 digits).

The 13-digit scale: Unix milliseconds#

With the rise of personal computing, graphical user interfaces, network latency tracing, and modern runtimes like Java (1995) and JavaScript (1995), second-level granularity proved insufficient. Animation frames, user click events, and database transaction timestamps required sub-second precision.

Java's System.currentTimeMillis() and JavaScript's Date.now() return milliseconds. For whole-second values, converting seconds to milliseconds multiplies by 1,000; real millisecond timestamps may use the final three digits for sub-second precision rather than simply ending in zeroes.

In the contemporary era, all millisecond timestamps are 13 decimal digits long (for example, 1704067200000).

Beyond milliseconds: Microseconds and nanoseconds#

High-performance computing and distributed tracing systems sometimes use even finer granularities:

  • Microseconds (16 digits in the contemporary era): One-millionth of a second. Some APIs and database representations use Unix microseconds; in Python, time.time_ns() // 1_000 produces an integer microsecond value.
  • Nanoseconds (19 digits in the contemporary era): One-billionth of a second. Go's time.Now().UnixNano() and Python's time.time_ns() return Unix-based nanosecond values. A monotonic clock such as CLOCK_MONOTONIC uses nanoseconds or another fine resolution but is not anchored to the Unix epoch.

Two infamous production bugs caused by unit mismatch#

When distributed systems exchange timestamps without explicit documentation of their scale, catastrophic bugs occur.

Bug 1: The "January 1970" frontend rendering bug#

This is the single most common date bug in web development.

A backend API (written in Python, Go, or PHP) returns a creation timestamp in seconds:

JSON
1{2  "id": "ord_98412",3  "createdAt": 4}

A frontend developer takes this timestamp and passes it directly into JavaScript's Date constructor:

JavaScript
1// BUG: JavaScript Date expects MILLISECONDS!2const date = new Date();3console.log(date.toISOString());4// Output: 1970-01-20T17:21:07.200Z

Why does this happen?

The developer intended to render January 1, 2024. However, because new Date(value) expects milliseconds, it interpreted 1704067200 as 1,704,067,200 milliseconds after the epoch.

Dividing 1,704,067,200 ms by 1,000 gives 1,704,067.2 seconds, or approximately 19.72 days after the Unix epoch. That lands on January 20, 1970.

An unexpected date in January 1970 is therefore a strong sign that a seconds value was interpreted as milliseconds.

Bug 2: Backend range and overflow errors#

The inverse error occurs when a frontend application or Java service sends a 13-digit millisecond timestamp to a backend service or database expecting seconds.

Suppose a client sends 1704067200000 to a Python backend:

Python
1import datetime23# BUG: Python's fromtimestamp expects SECONDS!4client_timestamp = 5date = datetime.datetime.fromtimestamp(client_timestamp, tz=datetime.timezone.utc)6print(date.isoformat())7# Typical result: OSError, OverflowError, or ValueError because the date8# is outside the platform or datetime range.

Likewise, inserting 1704067200000 into a signed 32-bit SQL INT column is out of range because the maximum is 2,147,483,647. Exact exception text depends on the database and SQL mode.

Ecosystem breakdown: Who uses what?#

Different programming languages, frameworks, and storage layers have hardened preferences. Knowing these defaults prevents cross-service integration failures:

Ecosystem / ToolNative UnitFunction / MethodExample Output (2026)
JavaScript / TypeScriptMillisecondsDate.now() / new Date().getTime()1790812800000
Browser elapsed-time APIMilliseconds since a monotonic origin, not Unix timeperformance.now()1423.8
JavaMillisecondsSystem.currentTimeMillis()1790812800000
PythonSeconds (Float)time.time()1790812800.41295
GoSeconds / Nanostime.Now().Unix() / UnixNano()1790812800
PHPSecondstime()1790812800
C / C++Secondstime(NULL)1790812800
RubySeconds (Float)Time.now.to_f1790812800.125
MySQL / MariaDBSecondsUNIX_TIMESTAMP()1790812800
PostgreSQLSeconds (Float)EXTRACT(EPOCH FROM NOW())1790812800.681
MongoDB ObjectIdTimestamp field stored with second precisionObjectId.getTimestamp() returns a date objectDate corresponding to creation time
Stripe APISecondscreated attribute1790812800

Notice the pattern: systems closer to the operating system, C runtime, or POSIX specifications use seconds. Systems designed for web browsers, user interfaces, or JVM runtimes use milliseconds.

Defensive heuristic detection for contemporary timestamps#

When your application ingests timestamps from third-party webhooks, arbitrary user CSV files, or legacy microservices, you may not know upfront whether an integer is in seconds, milliseconds, microseconds, or nanoseconds.

Magnitude can help with contemporary positive timestamps, but it cannot identify a unit universally. The safest API contract carries an explicit unit. If legacy input is unavoidable, reject values outside a documented date window instead of guessing every integer.

TEXT
1Threshold Detection Chart (Valid for years ~ to ~):2-----------------------------------------------------------3 digits  (,,, to ,,,)         -> SECONDS4 digits  (,,,, to ,,,,) -> MILLISECONDS5 digits  (^ to ^)                         -> MICROSECONDS6 digits  (^ to ^)                         -> NANOSECONDS

Defensive JavaScript and TypeScript example#

TypeScript
1export interface NormalizedTimestamp {2  milliseconds: bigint;3  date: Date;4  detectedUnit: "seconds" | "milliseconds" | "microseconds" | "nanoseconds";5}67export function normalizeContemporaryTimestamp(raw: string | bigint): NormalizedTimestamp {8  const value = BigInt(raw);9  const ranges = [10    { min: , max: , divisor: , multiplier: , unit: "seconds" },11    { min: , max: , divisor: , multiplier: , unit: "milliseconds" },12    { min: , max: , divisor: , multiplier: , unit: "microseconds" },13    {14      min: ,15      max: ,16      divisor: ,17      multiplier: ,18      unit: "nanoseconds",19    },20  ] as const;2122  const match = ranges.find(({ min, max }) => value >= min && value <= max);23  if (!match) throw new Error("Ambiguous timestamp: require an explicit unit");2425  const milliseconds = (value / match.divisor) * match.multiplier;26  const date = new Date(Number(milliseconds));27  if (Number.isNaN(date.getTime())) throw new Error("Timestamp is outside the Date range");2829  return { milliseconds, date, detectedUnit: match.unit };30}3132console.log(normalizeContemporaryTimestamp("1704067200").date.toISOString());33console.log(normalizeContemporaryTimestamp("1704067200000").date.toISOString());34// Both: 2024-01-01T00:00:00.000Z

Defensive Python example#

Python
1from datetime import datetime, timezone2from typing import Tuple34def parse_contemporary_timestamp(ts: int) -> Tuple[datetime, str]:5    ranges = (6        (**, ** - , , "seconds"),7        (**, ** - , , "milliseconds"),8        (**, ** - , , "microseconds"),9        (**, ** - , , "nanoseconds"),10    )1112    for minimum, maximum, divisor, unit in ranges:13        if minimum <= ts <= maximum:14            seconds = ts / divisor15            break16    else:17        raise ValueError("Ambiguous timestamp: require an explicit unit")1819    dt = datetime.fromtimestamp(seconds, tz=timezone.utc)20    return dt, unit2122# Tests:23dt1, unit1 = parse_contemporary_timestamp()24print(f"{dt1.isoformat()} (Detected: {unit1})")25# Output: 2024-01-01T00:00:00+00:00 (Detected: seconds)2627dt2, unit2 = parse_contemporary_timestamp()28print(f"{dt2.isoformat()} (Detected: {unit2})")29# Output: 2024-01-01T00:00:00+00:00 (Detected: milliseconds)

Best practices for API and database design#

To protect your software against timestamp corruption, adopt these architectural standards:

  1. Be explicit in field names: When naming JSON properties or database columns, include the unit in the identifier. Name your fields created_at_ms or expires_at_sec instead of ambiguous names like timestamp or time.
  2. Standardize on ISO 8601 for public APIs: For external REST and GraphQL APIs, use standardized ISO 8601 strings (e.g., 2026-10-01T12:00:00.000Z) rather than raw numbers. String dates are self-describing, human-readable in network logs, and eliminate all unit ambiguity.
  3. Use 64-bit integers for storage: Never store timestamps in 32-bit integer columns (INT). Always use BIGINT (8 bytes) to ensure compatibility with milliseconds and complete immunity to the Year 2038 Problem.
  4. Enforce UTC on the server: Always calculate and persist timestamps in UTC. Never apply local timezone offsets or daylight saving shifts to stored epoch values.

Frequently asked questions#

How do I know if my timestamp is in seconds or milliseconds?#

For contemporary positive dates, 10 digits usually indicates seconds and 13 digits usually indicates milliseconds. Treat that as a heuristic, not a universal rule: old, negative, or far-future timestamps need an explicit unit. A 1970 result from a modern-looking value passed to JavaScript's new Date() is a strong clue that seconds were supplied where milliseconds were expected.

Why does JavaScript default to milliseconds while Unix defaults to seconds?#

Unix was designed in 1969–1971 for operating systems and file systems where 1-second granularity was more than sufficient and memory was severely constrained (32-bit registers). JavaScript was designed in 1995 for interactive browser interfaces, user events, and animations, where sub-second millisecond timing is essential.

Can a Unix timestamp be negative?#

Yes. Negative timestamps represent dates prior to the Unix Epoch (January 1, 1970). For example, -315619200 corresponds to January 1, 1960 at 00:00:00 UTC. Standard 64-bit signed integer implementations safely support negative timestamps going back hundreds of millions of years.

Do Unix timestamps account for leap seconds?#

No. The POSIX standard explicitly mandates that every day consists of exactly 86,400 seconds. When an official leap second is inserted by the International Earth Rotation and Reference Systems Service (IERS), Unix clocks repeat a second or use "leap second smearing" (gradually slowing the system clock over a 24-hour window) to maintain synchronization with atomic time without adding a 61st second to the integer counter.

What happens when converting between float and integer timestamps?#

Python and PostgreSQL often represent timestamps as floating-point numbers (e.g., 1704067200.500), where the integer part represents seconds and the decimal represents fractional milliseconds or microseconds. When converting floats to integer milliseconds, always multiply first and then round or floor: Math.floor(floatSeconds * 1000). Converting float directly to int truncates the fractional precision.

Debug, test, and convert your timestamps and identifiers directly in your browser: