Timestamp in Seconds vs Milliseconds: The Mistake Developers Keep Making
Every developer has a timestamp horror story.
Mine happened during a late-night deployment. A payment service returned expiration times as Unix timestamps, and everything looked normal in the API response:
{
"expires_at": 1717000000
}
The frontend converted it:
new Date(1717000000);
The result? Tue Jan 20 1970 ...
The backend was correct. The frontend code looked correct. The timestamp was valid. But the payment system now thought transactions expired in 1970.
The problem: one system used seconds, the other expected milliseconds.
This bug appears on Stack Overflow daily. It shows up in GitHub issues, Reddit threads, and production incidents across the industry. The fix is a one-line change, but finding it can take hours if you don't know what to look for.
The Core Problem: Two Formats, Same Name
Developers often assume all timestamps are identical. They're not. Two formats dominate modern development.
Unix Timestamp in Seconds
1717000000 → 10 digits → 2024-05-29 16:53:20 UTC
Used by Python, PHP, MySQL, PostgreSQL, and most backend systems. This is the traditional Unix standard.
Unix Timestamp in Milliseconds
1717000000000 → 13 digits → 2024-05-29 16:53:20 UTC
Used by JavaScript and Java. Exactly the same moment. Completely different number.
The only visual difference is three extra zeros. The resulting date can differ by decades if you guess wrong.
Why Milliseconds Exist
Seconds work fine for database records, scheduled jobs, user activity logs, and authentication tokens. But modern applications often need higher precision.
Performance monitoring, analytics tracking, animation systems, real-time applications, and financial transactions all benefit from sub-second granularity. Milliseconds let applications distinguish events occurring within the same second.
The tradeoff: milliseconds are more precise, but they break compatibility with every system that expects seconds.
The JavaScript Bug That Catches Everyone
JavaScript is responsible for introducing millions of developers to this problem. Its Date constructor expects milliseconds, but most APIs and backends return seconds.
const timestamp = 1717000000;
console.log(new Date(timestamp));
// Expected: 2024-05-29
// Actual: 1970-01-20
The fix:
const timestamp = 1717000000;
console.log(new Date(timestamp * 1000));
// → Wed May 29 2024 ...
That * 1000 is the difference between a working application and a production incident. I've debugged this exact issue at least a dozen times across different codebases.
If you're new to timestamps and want the full background on how they work, start with what is a Unix timestamp.
A Quick Rule That Saves Hours
When debugging timestamps, count the digits first.
10 digits → seconds. Example: 1717000000
13 digits → milliseconds. Example: 1717000000000
A simple detection function:
function isMilliseconds(timestamp) {
return timestamp.toString().length === 13;
}
def is_milliseconds(timestamp):
return len(str(timestamp)) == 13
This five-second check has saved me more times than I can count. Before diving into complex debugging, verify the unit.
How APIs Create This Problem
Most APIs return timestamps without explicitly documenting the unit:
{
"created_at": 1717000000
}
The backend engineer knows it's seconds. The frontend engineer assumes milliseconds. Nobody notices until production.
Good API documentation always specifies the unit explicitly: created_at (Unix timestamp in seconds) or created_at (Unix timestamp in milliseconds). Never leave it ambiguous.
Real Production Example: Authentication
A login system stores session expiration times.
Backend (Python):
import time
expires_at = int(time.time()) + 3600
# → 1717003600 (one hour from now)
Frontend (JavaScript):
const expiresAt = response.expires_at;
const date = new Date(expiresAt);
// → 1970
Users see "Session expired 54 years ago." The bug isn't in authentication logic. The bug isn't in the timestamp value. The bug is unit conversion.
Correct version:
const date = new Date(expiresAt * 1000);
How Each Language Handles This
Knowing the defaults prevents confusion.
JavaScript
Date.now();
// → 1717000000000 (milliseconds)
Python
import time
time.time()
# → 1717000000.123 (seconds, with float sub-second)
PHP
time();
// → 1717000000 (seconds)
Java
System.currentTimeMillis();
// → 1717000000000 (milliseconds)
Instant.now().getEpochSecond();
// → 1717000000 (seconds — the safer choice)
Go
time.Now().Unix()
// → 1717000000 (seconds)
time.Now().UnixMilli()
// → 1717000000000 (milliseconds — explicit naming)
The inconsistency is the root cause. JavaScript and Java default to milliseconds. Python and PHP default to seconds. When these systems talk to each other, unit mismatches are inevitable unless someone explicitly converts.
Database Confusion
Databases add another layer of potential mismatch.
MySQL
SELECT UNIX_TIMESTAMP();
-- → 1717000000 (seconds)
PostgreSQL
SELECT EXTRACT(EPOCH FROM NOW());
-- → 1717000000.123456 (seconds, with microsecond precision)
MongoDB
Stores dates internally with millisecond precision. When MongoDB interacts with a Node.js backend via Mongoose, timestamps flow as milliseconds without explicit conversion. That's usually fine — until the same data reaches a Python microservice that expects seconds.
Converting Between Seconds and Milliseconds
Seconds → Milliseconds
const milliseconds = seconds * 1000;
1717000000 becomes 1717000000000. Same moment, compatible with JavaScript.
Milliseconds → Seconds
const seconds = Math.floor(milliseconds / 1000);
1717000000000 becomes 1717000000. Compatible with Python, PHP, and most databases.
Always Math.floor() rather than direct division. Fractional timestamps can cause subtle off-by-one errors in comparisons.
Debugging Timestamp Issues Systematically
When a timestamp looks suspicious, follow these steps:
-
Count the digits. 10 digits = seconds. 13 digits = milliseconds. This alone catches most issues.
-
Convert it. Paste the value into a timestamp converter. A quick conversion usually reveals whether the value is valid and what unit it represents.
-
Check language expectations. Does this API return seconds? Does this framework expect milliseconds? Does this database driver perform automatic conversion?
-
Verify timezone separately. Many developers blame timezones when the real issue is a unit mismatch. A timestamp showing 1970 is almost always a seconds-vs-milliseconds bug, not a timezone problem.
Why This Bug Keeps Happening
Three factors keep this bug alive:
-
Different language defaults. JavaScript uses milliseconds. Python uses seconds. PHP uses seconds. Java often uses milliseconds. The inconsistency is baked into the ecosystem.
-
Poor API documentation. Most APIs return a number and call it a "timestamp" without specifying the unit. Two teams reading the same documentation can make different assumptions.
-
Copy-pasted code. Developers copy snippets from Stack Overflow without verifying assumptions. Code that works in a Python backend fails silently when pasted into a Node.js service.
Best Practices
Always Document Units
Instead of {"created_at": 1717000000}, document created_at: Unix timestamp in seconds. Three extra words prevent hours of confusion.
Validate Upfront
Before conversion, normalize:
function toSeconds(timestamp) {
return timestamp.toString().length >= 13
? Math.floor(timestamp / 1000)
: timestamp;
}
Standardize Across Services
Choose one format for your entire platform. Most backend-heavy teams standardize on seconds and convert to milliseconds only at the JavaScript boundary.
Use Conversion Tools During Debugging
Rather than mentally decoding a 10- or 13-digit number, paste it into the DevFormatters Timestamp Converter to verify the value instantly. It's faster than writing throwaway console.log statements.
Related Resources
- What Is a Unix Timestamp? — the fundamentals of Unix time
- Why Timezones Break Your Applications — when timestamps meet real-world time
- How to Convert Unix Timestamps in JavaScript, Python, PHP, and SQL — practical code examples for every language
Frequently Asked Questions
How do I know if a timestamp is in seconds or milliseconds?
Count the digits. 10 digits is usually seconds. 13 digits is usually milliseconds.
Why does JavaScript use milliseconds?
JavaScript's Date API was designed for higher precision than seconds. It's been this way since the language was created.
Why does my timestamp show a date in 1970?
You're almost certainly passing a seconds-based timestamp to a function that expects milliseconds. Multiply by 1000.
Should APIs use seconds or milliseconds?
Either works. Seconds are more compatible with backend systems and databases. The important thing is documenting which one you use and being consistent.
How do I convert seconds to milliseconds?
Multiply by 1000: milliseconds = seconds * 1000
How do I convert milliseconds to seconds?
Divide by 1000: seconds = Math.floor(milliseconds / 1000)
Are timezones related to this problem?
Not directly. Most timestamp bugs involving 1970 dates are unit conversion issues, not timezone handling. If you're seeing dates 54 years in the past, check your units first.
The seconds-vs-milliseconds mistake seems trivial on paper. In production, it breaks authentication, corrupts analytics data, and displays impossible dates to users.
Once you've encountered it a few times, you start spotting it immediately. A glance at the digit count is often enough to identify the problem before it ships.
Whenever a timestamp looks suspicious, convert it, verify the unit, and check what your language expects. Those three steps eliminate the vast majority of timestamp-related bugs. If you regularly work with APIs, JWT tokens, databases, or analytics systems, the DevFormatters Timestamp Converter is often faster than mentally decoding timestamps during an active incident.