Unix Seconds vs Milliseconds: The Bug That Haunts Every Developer
You paste a timestamp into your code, the result shows January 1, 1970. Or worse — a date 50,000 years in the future. This is the most common timestamp bug in software, and it has one cause: confusing Unix seconds with Unix milliseconds. Here's everything you need to know to never hit it again.
The Core Difference
Unix time counts how much time has passed since January 1, 1970 at 00:00:00 UTC. The question is: in what unit?
Date.now(), Java, Kotlin, Android, many analytics platforms.Milliseconds offer 1,000× more precision than seconds. The difference matters for sub-second event ordering, high-frequency systems, and any log that needs to distinguish events happening within the same second.
Why You See January 1970 — The Classic Bug
This happens when you pass Unix seconds to a function that expects milliseconds — or vice versa.
// ❌ WRONG — passing Unix seconds to new Date() which expects milliseconds
const ts = 1715429912; // Unix seconds from your API
new Date(ts).toISOString();
// → "1970-01-20T20:30:29.912Z" ← 20 days after epoch, not 2026!
// ✅ CORRECT — multiply by 1000 first
new Date(ts * 1000).toISOString();
// → "2026-05-11T14:38:32.000Z" ← correct
The inverse bug also exists. Passing Unix milliseconds where seconds are expected gives you a date in the year 58,000+. This happens often when storing Date.now() in a database that expects Unix seconds.
How to Detect Precision Instantly — The Digit Rule
The simplest rule, which works reliably between 2001 and 2286:
| Digits | Precision | Example | Year range |
|---|---|---|---|
| 9 | Unix seconds | 999999999 | September 2001 |
| 10 | Unix seconds | 1715429912 | 2001 – 2286 |
| 13 | Unix milliseconds | 1715429912000 | 2001 – 2286 |
| 16 | Unix microseconds | 1715429912000000 | PostgreSQL, ClickHouse |
| 19 | Unix nanoseconds | 1715429912000000000 | Go, Rust, InfluxDB |
Quick check: count the digits. 10 = seconds. 13 = milliseconds. If it's neither, paste it into UnixLi — it detects the precision automatically and warns you if the result looks wrong.
Auto-Detection in Code
Here's a reliable detection function you can use in any project:
/**
* Normalizes any Unix timestamp to milliseconds.
* Handles seconds (10 digits), milliseconds (13 digits),
* and microseconds (16 digits).
*/
function toMilliseconds(ts) {
const n = Number(ts);
if (isNaN(n)) throw new Error('Invalid timestamp');
const digits = Math.abs(n).toString().split('.')[0].length;
if (digits <= 10) return n * 1000; // seconds → ms
if (digits <= 13) return n; // already ms
if (digits <= 16) return Math.floor(n / 1000); // microseconds → ms
return Math.floor(n / 1_000_000); // nanoseconds → ms
}
// Usage — works with any precision
new Date(toMilliseconds(1715429912)); // seconds ✅
new Date(toMilliseconds(1715429912000)); // milliseconds ✅
new Date(toMilliseconds("1715429912")); // string ✅
from datetime import datetime, timezone
def to_seconds(ts: int | float) -> float:
"""Normalize any Unix timestamp to seconds."""
digits = len(str(abs(int(ts))))
if digits <= 10: return float(ts) # already seconds
if digits <= 13: return ts / 1_000 # ms → seconds
if digits <= 16: return ts / 1_000_000 # µs → seconds
return ts / 1_000_000_000 # ns → seconds
# Usage
dt = datetime.fromtimestamp(to_seconds(1715429912000), tz=timezone.utc)
# → 2026-05-11 14:38:32+00:00 ✅
Language-by-Language Reference
Every language and ecosystem has its own default precision. This table is the definitive reference.
| Language / System | Default precision | How to get Unix seconds | How to get Unix ms |
|---|---|---|---|
| JavaScript | Milliseconds | Math.floor(Date.now() / 1000) |
Date.now() |
| Python | Seconds (float) | int(time.time()) |
time.time_ns() // 1_000_000 |
| PHP | Seconds | time() |
(int)(microtime(true) * 1000) |
| Go | Seconds (via .Unix()) |
time.Now().Unix() |
time.Now().UnixMilli() |
| Rust | Seconds + nanoseconds | now.as_secs() |
now.as_millis() |
| Java / Kotlin | Milliseconds | System.currentTimeMillis() / 1000 |
System.currentTimeMillis() |
| PostgreSQL | Seconds (float) | EXTRACT(EPOCH FROM NOW())::BIGINT |
(EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT |
| MongoDB | Milliseconds (ISODate) | Math.floor(new Date().getTime() / 1000) |
new Date().getTime() |
| Redis | Seconds | TIME command (returns seconds + microseconds) |
Combine TIME fields |
JWT (exp, iat) |
Seconds | Always seconds per RFC 7519 | N/A — spec mandates seconds |
| Discord timestamps | Seconds | <t:TIMESTAMP:R> uses seconds |
N/A |
JWT gotcha: RFC 7519 mandates that exp, iat, and nbf are always Unix seconds — never milliseconds. If you store Date.now() as exp, your token will appear valid for 500 years.
Identifying Precision in API Responses
When you receive a timestamp from a third-party API and the documentation is unclear, apply this flowchart mentally:
function diagnoseTimestamp(ts) {
const asSeconds = new Date(ts * 1000);
const asMs = new Date(ts);
const nowYear = new Date().getUTCFullYear();
const secYear = asSeconds.getUTCFullYear();
const msYear = asMs.getUTCFullYear();
console.log(`As seconds: ${asSeconds.toISOString()} (${secYear})`);
console.log(`As ms: ${asMs.toISOString()} (${msYear})`);
// Whichever year is closest to now is the right interpretation
const secDiff = Math.abs(secYear - nowYear);
const msDiff = Math.abs(msYear - nowYear);
return secDiff < msDiff ? 'seconds' : 'milliseconds';
}
diagnoseTimestamp(1715429912);
// As seconds: 2026-05-11T14:38:32.000Z (2026) ← closest to now
// As ms: 1970-01-20T20:30:29.912Z (1970)
// → "seconds"
Best Practices — Never Hit This Bug Again
- Always be explicit in variable names. Name your variables
createdAtSecorcreatedAtMs, never justcreatedAt. Your future self and teammates will thank you. - Use TypeScript branded types.
type UnixSeconds = number & {'{'} _brand: 'UnixSeconds' {'}'}makes it a compile error to mix them. See the JavaScript guide for implementation. - Document your API contracts explicitly. In your OpenAPI / Swagger spec, always specify whether a timestamp field is seconds or milliseconds. Never leave it implicit.
- Validate on input. When receiving timestamps from external sources, run the digit-count check and reject values that resolve to implausible dates (before 2000 or after 2100 for most applications).
- Pick one and stick to it. Within a single codebase, standardize on Unix seconds (simpler, universally supported) or milliseconds (JavaScript-native). Never mix both without explicit conversion functions.
Not sure which one your timestamp is?
Paste it into UnixLi — it auto-detects precision in milliseconds, warns you if the result looks wrong, and shows you the conversion in every format at once.
Detect precision instantly →Quick Reference
| Goal | JavaScript | Python |
|---|---|---|
| Now in seconds | Math.floor(Date.now()/1000) | int(time.time()) |
| Now in milliseconds | Date.now() | time.time_ns()//1_000_000 |
| Seconds → ms | ts * 1000 | ts * 1000 |
| Ms → seconds | Math.floor(ts / 1000) | ts / 1000 |
| Seconds → Date | new Date(ts * 1000) | datetime.fromtimestamp(ts, utc) |
| Ms → Date | new Date(ts) | datetime.fromtimestamp(ts/1000, utc) |
| Detect precision | ts.toString().length <= 10 ? 'sec' : 'ms' | len(str(ts)) <= 10 |
Summary
The rule is simple: count the digits. 10 digits = Unix seconds. 13 digits = Unix milliseconds. Any function that takes milliseconds (like JavaScript's new Date()) needs seconds multiplied by 1000. Any function that takes seconds (like Python's datetime.fromtimestamp() or JWT's exp) needs milliseconds divided by 1000.
Write the toMilliseconds() normalization helper once, put it in your utils, and never debug a 1970 date again.
Next up: the complete guide to Unix timestamps in JavaScript — formatting, timezones, TypeScript types, and every conversion you'll need.