Telling Seconds from Milliseconds in Epoch Values

The reliable way to tell a seconds epoch from a milliseconds epoch is to know the source and convert at the boundary; digit-counting is only a last-resort heuristic. Part of Unix Timestamps & Epoch Conversion.

Why This Scenario Is Tricky

A raw integer carries no unit. 1700000000 and 1700000000000 both look like "a timestamp", but one is seconds and one is milliseconds, and JavaScript's new Date() blindly treats either as milliseconds. The popular shortcut — "10 digits means seconds, 13 means milliseconds" — works today but is not a contract. It quietly breaks on real data:

The correct mental model: the unit is metadata about the source, not something recoverable from the value. Capture it once, at the point of ingestion, and carry it explicitly.

Seconds versus milliseconds scale comparisonTwo stacked scales, one labelled seconds and one milliseconds, showing the same instant at different magnitudes and a caution that digit count is not a reliable unit signal.Same instant, two magnitudesseconds (×1)1700000000 — 10 digitsmilliseconds (×1000)1700000000000 — 13 digitsDigit count distinguishes them only within today's era — prefer the known source unit.

Minimal Working Solution

When you control the source, convert explicitly and stop there:

// You KNOW this column is seconds (e.g. Postgres extract(epoch from ts)).
const seconds = 1700000000;
const date = new Date(seconds * 1000); // declare the unit by scaling at the boundary

When you genuinely cannot know the unit, use a magnitude heuristic — but anchor it to a date window, not a digit count, and document it as a fallback:

// Fallback only: assume any value below the year-2001-in-seconds threshold,
// when read as MILLISECONDS, would predate ~1970 absurdly — so treat large
// values as ms and small values as seconds, using a fixed cutover instant.
const MS_CUTOVER = 1e11; // 1970-01-01 + 1e11 ms ≈ 2001; values above are almost surely ms
function coerceToMs(value: number): number {
  return value < MS_CUTOVER ? value * 1000 : value;
}

Full Production Version

The robust design makes the unit an explicit parameter and treats the heuristic as an opt-in fallback that logs when it has to guess. It validates the result lands inside a sane date window so a misclassification fails loudly.

import { Temporal } from '@js-temporal/polyfill';

type EpochUnit = 'seconds' | 'milliseconds' | 'auto';

const MS_CUTOVER = 1e11; // ~year 2001 in ms; below this, a ms value would be pre-2001
const MIN_MS = Temporal.Instant.from('2000-01-01T00:00:00Z').epochMilliseconds;
const MAX_MS = Temporal.Instant.from('2100-01-01T00:00:00Z').epochMilliseconds;

function toInstant(value: number, unit: EpochUnit = 'auto'): Temporal.Instant {
  if (!Number.isFinite(value)) {
    throw new TypeError(`Epoch value must be finite, got ${value}`);
  }
  let ms: number;
  if (unit === 'seconds') {
    ms = value * 1000;
  } else if (unit === 'milliseconds') {
    ms = value;
  } else {
    // 'auto' is a last resort — scale by magnitude and record that we guessed.
    ms = value < MS_CUTOVER ? value * 1000 : value;
    console.warn(`toInstant: guessed unit for ${value}${ms}ms; pass an explicit unit`);
  }
  if (ms < MIN_MS || ms > MAX_MS) {
    // Out-of-window result means the unit was almost certainly wrong.
    throw new RangeError(`Resolved ${ms}ms is outside 2000–2100; check the source unit`);
  }
  return Temporal.Instant.fromEpochMilliseconds(Math.trunc(ms));
}

Verification Snippet

import { Temporal } from '@js-temporal/polyfill';

// Explicit units resolve to the same instant — this is the contract you want.
const fromSec = Temporal.Instant.fromEpochMilliseconds(1700000000 * 1000);
const fromMs = Temporal.Instant.fromEpochMilliseconds(1700000000000);
console.assert(fromSec.equals(fromMs), 'scaled seconds must equal the ms value');

// The 'auto' heuristic classifies modern values correctly...
console.assert(
  toInstant(1700000000, 'auto').epochSeconds === 1700000000,
  'modern 10-digit value should be read as seconds',
);
console.assert(
  toInstant(1700000000000, 'auto').epochSeconds === 1700000000,
  'modern 13-digit value should be read as milliseconds',
);

// ...but a misclassified unit is rejected, proving the window guard works.
let threw = false;
try { toInstant(1700000000000, 'seconds'); } catch { threw = true; }
console.assert(threw, 'ms value labelled as seconds should fall outside 2000–2100');

Common Pitfalls

const isMs = String(value).length === 13; // WRONG: breaks across eras and on fractions
toInstant(value, sourceUnit);             // RIGHT: pass the known unit

Frequently Asked Questions

How can I tell if a timestamp is in seconds or milliseconds?

Reliably, you cannot from the value alone — the unit is a property of the source. Capture it at ingestion and pass it explicitly. If you must guess, scale by magnitude against a fixed cutover instant rather than counting digits, and validate the result lands in a sane date range so a wrong guess fails loudly.

Is the "10 digits vs 13 digits" rule safe?

Only within the current era. Seconds crossed to 10 digits in 2001 and milliseconds reach 14 digits in 2286, so the rule has hard boundaries and also misbehaves on fractional-second values. Use it only as a documented fallback with a range check, never as a contract.