If you've worked on web applications for any length of time, you've probably run into a timestamp bug that made no obvious sense at first. A date showing up as 1970. A time displaying as 52 years in the future. An API rejecting a timestamp that looks perfectly reasonable.
There's a good chance the culprit was the seconds vs milliseconds mismatch — one of the most common date-handling bugs in software, and one of the easiest to miss.
The Core Problem
Unix timestamps are supposed to count seconds since January 1, 1970 (the Unix epoch). That's the original standard, and it's what virtually every server-side language and system uses:
- Python's
time.time()returns seconds - PHP's
time()returns seconds - Most SQL databases store and return seconds
- The Unix shell command
date +%sreturns seconds - Most REST API documentation, when it says "Unix timestamp," means seconds
JavaScript does something different. Date.now() and new Date().getTime() return milliseconds — 1000 times the equivalent Unix timestamp.
This means a timestamp value like 1712534400 represents April 8, 2024 in seconds. But 1712534400000 represents the same moment in milliseconds — and if you accidentally use the milliseconds value where seconds are expected, or vice versa, you get dates that are wildly wrong.
What the Bugs Actually Look Like
Scenario 1: JavaScript milliseconds sent to a seconds-expecting API
A frontend sends Date.now() — say, 1712534400000 — directly to a backend that stores it as a Unix timestamp in seconds. The backend stores 1712534400000 as the timestamp. Convert that back to a date and you get somewhere around the year 56000.
This is the "date showing up in the far future" bug.
Scenario 2: Seconds value used in JavaScript
A backend returns a Unix timestamp in seconds — say, 1712534400. A JavaScript developer passes it directly to new Date(1712534400). JavaScript interprets this as 1,712,534,400 milliseconds since the epoch — about 19.8 days after the epoch. The UI displays a date in January 1970.
This is the "date showing up in 1970" bug.
Scenario 3: JWT expiry set in the wrong unit
A JSON Web Token is issued with an exp claim intended to expire in 24 hours. The developer calculates Date.now() + 86400 — intending to add 86,400 seconds (24 hours). But Date.now() is in milliseconds, and 86,400 is in seconds. The result is a timestamp about 86,400 milliseconds (86.4 seconds) in the future. Every token expires in under 2 minutes instead of 24 hours.
Scenario 4: Database timestamp read back wrongly
A PostgreSQL column stores a Unix timestamp as an integer (seconds). The value is fetched and used in JavaScript as-is, producing a date in 1970. Fix: multiply by 1000 before passing to new Date().
You can use the Unix timestamp converter to quickly check whether a number you're looking at is a seconds or milliseconds timestamp and what date it corresponds to — useful for debugging.
How to Tell Seconds from Milliseconds
The fastest way: count the digits.
- 10 digits (e.g.,
1712534400) → seconds timestamp. Represents a date in the plausible present or recent past/future. - 13 digits (e.g.,
1712534400000) → milliseconds timestamp.
A seconds-based Unix timestamp for dates from 2000 to 2050 runs from about 946,684,800 to 2,524,608,000 — always 10 digits. If you see a 13-digit integer in a time-related context, it's almost certainly milliseconds.
This rule works for current dates. Far-future dates (post-year 2286) would have 11-digit second timestamps, but that's an edge case you can ignore for practical purposes.
The Fix in Each Language
JavaScript (convert to seconds when sending to backend): `javascript const timestampSeconds = Math.floor(Date.now() / 1000); `
JavaScript (convert seconds from backend for use in Date): `javascript const date = new Date(timestampSeconds * 1000); `
Python (already in seconds): `python import time timestamp_seconds = int(time.time()) `
PHP (already in seconds): `php $timestamp_seconds = time(); `
SQL (PostgreSQL, extract seconds): `sql SELECT EXTRACT(EPOCH FROM NOW())::int; `
Converting a milliseconds timestamp to seconds (any language): ` divide by 1000, then floor/truncate `
JWT Expiry: The Right Way
JWT claims (exp, iat, nbf) must be in seconds per the RFC 7519 specification. In JavaScript:
const now = Math.floor(Date.now() / 1000); // seconds
const exp = now + 86400; // 24 hours from now, in seconds
Not: `javascript const exp = Date.now() + 86400; // wrong: mixes milliseconds base with seconds offset `
And not: `javascript const exp = Date.now() + (86400 * 1000); // wrong: exp would be ~28 days in the future in milliseconds `
A JWT with an exp value that's 13 digits will be rejected by most JWT libraries as invalid or far-future. The Unix timestamp tool can help you verify what a specific exp value decodes to.
Storing Timestamps in Databases
When designing schemas, be consistent about the unit:
- If you store seconds, document it. Column names like
created_at_unixor a comment clarifying "Unix timestamp in seconds" prevent ambiguity. - If your ORM or framework stores milliseconds (some JavaScript ORMs do), be explicit about that too.
- Avoid mixing — don't store some timestamps in seconds and others in milliseconds in the same system.
PostgreSQL's native TIMESTAMPTZ type is often a better choice than storing raw integers, as it handles timezone conversion and formatting natively. But when you do use integers for timestamps, settle on one unit and stick to it.
A Note on Microseconds
Some systems go even further and use microseconds (millionths of a second). Python's time.time_ns() returns nanoseconds. High-frequency trading systems, profiling tools, and some database internals use microsecond precision.
If you see a 16-digit integer in a time context, it might be microseconds. A 19-digit integer might be nanoseconds. The same detection heuristic applies — digit count tells you the approximate scale.
For web application timestamps, milliseconds is as precise as you'll ever need. Microsecond precision matters only in performance-critical, high-frequency systems.
The Practical Rule
Whenever a timestamp crosses a language or system boundary — JavaScript frontend to Python/PHP/Go/Java backend, application to database, application to external API — ask: which side uses seconds and which uses milliseconds? Add the conversion at the boundary, document it, and test with a known date to verify the result makes sense.
The test is simple: convert the timestamp you're generating and make sure it shows a plausible date. If it shows 1970 or 55000, you know immediately something is off.


