Unix Timestamp: Milliseconds vs Seconds - Avoid the Off-By-1000 Bug
Your API returns a timestamp. You pass it to new Date(). The result shows January 1, 1970 instead of today's date.
Or worse: your calendar widget displays events in the year 53,000 AD.
This isn't science fiction. It's the milliseconds vs seconds bugβone of the most common (and maddening) time-related errors in programming.
This guide explains exactly what causes it, how to detect it instantly, and how to prevent it in your code.
The Bug: When Timestamps Are Off By 1000x
Symptoms
Problem 1: Dates stuck in 1970
const timestamp = 1609459200; // Your API returns this
const date = new Date(timestamp);
console.log(date); // 1970-01-19 (WRONG!)
Problem 2: Dates in the distant future
const timestamp = 1609459200000; // Your database returns this
const unixSeconds = timestamp; // You forget to divide by 1000
// Result: Year 53,000+ dates
Problem 3: "Invalid Date" errors
const timestamp = "1609459200"; // String instead of number
const date = new Date(timestamp); // NaN or Invalid Date
Root Cause: Two Different Unix Timestamp Standards
Unix timestamps measure time since January 1, 1970, 00:00:00 UTC (the "Unix Epoch").
But there are two units:
| Unit | Example | Digit Count | Used By |
|---|---|---|---|
| Seconds | 1609459200 |
10 digits | Python, Unix date, PHP, PostgreSQL TIMESTAMP |
| Milliseconds | 1609459200000 |
13 digits | JavaScript, Java, MySQL UNIX_TIMESTAMP() * 1000 |
The problem:
- JavaScript's
Date()expects milliseconds - Most other systems use seconds
- Mixing them causes 1000x errors
How to Detect Which Format You Have
Method 1: Count the Digits
const timestamp = 1609459200; // Your value
if (timestamp.toString().length === 10) {
console.log("Seconds format");
// Convert to milliseconds: timestamp * 1000
} else if (timestamp.toString().length === 13) {
console.log("Milliseconds format");
// Use directly in JavaScript
} else {
console.error("Invalid timestamp");
}
Method 2: Check If Date Is Reasonable
function isValidTimestamp(timestamp) {
const date = new Date(timestamp);
const year = date.getFullYear();
// Reasonable range: 1970-2100
return year >= 1970 && year <= 2100;
}
// Test
isValidTimestamp(1609459200); // false (1970)
isValidTimestamp(1609459200000); // true (2021)
Method 3: Use a Timestamp Converter Tool
Paste your timestamp into the free DailyToolbox Unix Timestamp Converter:
- Seconds: Shows a recent date
- Milliseconds: Shows a date 1000x later (or in 1970)
Common Scenarios Where This Bug Appears
Scenario 1: JavaScript β Python API
Python backend (returns seconds):
import time
timestamp = int(time.time()) # 1609459200 (10 digits)
return {"created_at": timestamp}
JavaScript frontend (expects milliseconds):
fetch('/api/data')
.then(res => res.json())
.then(data => {
// BUG: created_at is in seconds, but Date() expects milliseconds
const date = new Date(data.created_at); // Shows 1970!
// FIX: Multiply by 1000
const date = new Date(data.created_at * 1000); // Correct!
});
Scenario 2: SQL TIMESTAMP β JavaScript
PostgreSQL (stores seconds):
SELECT EXTRACT(EPOCH FROM created_at) AS unix_time FROM orders;
-- Returns: 1609459200 (10 digits)
JavaScript (expects milliseconds):
const rows = await db.query('SELECT ...');
const timestamp = rows[0].unix_time;
// BUG
const date = new Date(timestamp); // 1970!
// FIX
const date = new Date(timestamp * 1000); // Correct
Scenario 3: Redis TTL β JavaScript setTimeout
Redis (uses seconds):
// Set key to expire in 3600 seconds (1 hour)
await redis.set('session:123', data, 'EX', 3600);
JavaScript (expects milliseconds):
const ttl = await redis.ttl('session:123'); // Returns 3600 (seconds)
// BUG: setTimeout expects milliseconds
setTimeout(() => {
console.log('Session expired');
}, ttl); // Fires after 3.6 seconds instead of 1 hour!
// FIX: Convert to milliseconds
setTimeout(() => {
console.log('Session expired');
}, ttl * 1000); // Correct: 1 hour
Scenario 4: Inconsistent API Documentation
API docs say:
"Returns Unix timestamp"
But which format?
- Could be seconds
- Could be milliseconds
- Could be microseconds (rare, but some systems use it)
Solution: Always test with actual data and check if the date makes sense.
How to Fix: Universal Timestamp Handling Function
Use this helper to automatically detect and convert:
function parseTimestamp(timestamp) {
// Handle string inputs
if (typeof timestamp === 'string') {
timestamp = parseInt(timestamp, 10);
}
// Detect format by digit count
const digits = timestamp.toString().length;
if (digits === 10) {
// Seconds β convert to milliseconds
return new Date(timestamp * 1000);
} else if (digits === 13) {
// Milliseconds β use directly
return new Date(timestamp);
} else if (digits === 16) {
// Microseconds (rare) β convert to milliseconds
return new Date(timestamp / 1000);
} else {
throw new Error(`Invalid timestamp: ${timestamp}`);
}
}
// Usage
parseTimestamp(1609459200); // Correctly handles seconds
parseTimestamp(1609459200000); // Correctly handles milliseconds
parseTimestamp("1609459200"); // Correctly handles strings
Prevention: Best Practices
β 1. Always Document Timestamp Units in APIs
Bad:
{
"created_at": 1609459200
}
Good:
{
"created_at_seconds": 1609459200,
"created_at_ms": 1609459200000
}
Or use descriptive field names:
{
"created_at_unix": 1609459200, // Seconds
"created_at_epoch_ms": 1609459200000 // Milliseconds
}
β 2. Use ISO 8601 Strings Instead
Avoid the problem entirely:
{
"created_at": "2021-01-01T00:00:00Z" // ISO 8601
}
Advantages:
- Human-readable
- No ambiguity
- Works across all languages
- Includes timezone info
JavaScript handling:
const date = new Date("2021-01-01T00:00:00Z"); // Works perfectly
β 3. Add Unit Suffix to Variable Names
// BAD (ambiguous)
const timestamp = 1609459200;
// GOOD (clear)
const timestampSeconds = 1609459200;
const timestampMs = 1609459200000;
β 4. Write Tests for Edge Cases
describe('Timestamp handling', () => {
it('should handle seconds format', () => {
const date = parseTimestamp(1609459200);
expect(date.getFullYear()).toBe(2021);
});
it('should handle milliseconds format', () => {
const date = parseTimestamp(1609459200000);
expect(date.getFullYear()).toBe(2021);
});
it('should reject invalid timestamps', () => {
expect(() => parseTimestamp(123)).toThrow();
});
});
Real-World Examples
Example 1: Node.js API That Returns Seconds
// Backend: Express.js
app.get('/api/user', (req, res) => {
const user = {
id: 123,
created_at: Math.floor(Date.now() / 1000) // Seconds
};
res.json(user);
});
// Frontend: React
fetch('/api/user')
.then(res => res.json())
.then(user => {
// Must multiply by 1000
const date = new Date(user.created_at * 1000);
console.log(date.toLocaleString());
});
Example 2: Python β JavaScript Data Pipeline
Python script exports CSV:
import time
import csv
with open('events.csv', 'w') as f:
writer = csv.writer(f)
writer.writerow(['event_id', 'timestamp'])
writer.writerow([1, int(time.time())]) # Seconds
JavaScript reads CSV:
const fs = require('fs');
const csv = require('csv-parser');
fs.createReadStream('events.csv')
.pipe(csv())
.on('data', (row) => {
// Must convert to milliseconds
const date = new Date(row.timestamp * 1000);
console.log(date);
});
FAQ
Q: Why does JavaScript use milliseconds when everyone else uses seconds?
A: Historical reasons. JavaScript's Date object was designed in the 1990s to match Java's java.util.Date, which uses milliseconds.
Q: How do I get the current Unix timestamp in JavaScript?
A: Seconds: Math.floor(Date.now() / 1000)
Milliseconds: Date.now()
Q: What about timezone issues?
A: Unix timestamps are always UTC. Timezones only matter when you display the date to users. Use toLocaleString() or libraries like Luxon/date-fns for timezone handling.
Q: Can I use Date.parse() instead of new Date()?
A: Date.parse() expects an ISO 8601 string, not a Unix timestamp. Use new Date(timestamp) for numeric timestamps.
Debugging Checklist
When you see a date bug:
β Is the timestamp in seconds or milliseconds?
β Did you multiply/divide by 1000 correctly?
β Is the timestamp a string instead of a number?
β Does the API documentation specify the unit?
β Does the date make logical sense (between 1970-2100)?
β Are you mixing UTC and local time?
Conclusion
The milliseconds vs seconds bug is 100% preventable. The solution is simple:
- Always document which format you're using
- Use ISO 8601 strings when possible
- Auto-detect the format with digit counting
- Test with real data before deploying
Remember: A 10-digit timestamp is seconds. A 13-digit timestamp is milliseconds. Everything else is probably wrong.