Unix Timestamp Seconds vs Milliseconds: The 1000x Bug
Debug date values that land decades in the past or future by checking whether an API timestamp uses seconds, milliseconds, UTC or local display formatting.
Quick Answer
The classic timestamp bug is treating seconds as milliseconds or milliseconds as seconds. JavaScript Date expects milliseconds, while many APIs and databases expose Unix time in seconds. Count the digits, check the unit in the contract and convert deliberately before formatting the date.
Example Scenario
A subscription renewal date appears in 1970, a log event appears thousands of years in the future, or a cache expiration is immediately expired in one service but valid in another. The value is numeric and looks plausible, but one layer interpreted the unit differently from the layer that produced it.
Step-by-Step Explanation
- Count the digits in the timestamp before converting it.
- Check whether the API contract says seconds, milliseconds, microseconds or ISO string.
- Convert explicitly at the boundary where the value enters JavaScript.
- Format the result in UTC first, then compare local display if needed.
- Inspect database, API and frontend examples side by side.
- Add tests using real timestamps near the current date instead of tiny sample numbers.
The Digit Count Is the Fastest Clue
A Unix timestamp in seconds for modern dates is usually 10 digits. A JavaScript millisecond timestamp is usually 13 digits. Microseconds may be 16 digits. Nanoseconds may be 19 digits. Counting digits is not a formal contract, but it is a fast way to spot a unit mismatch before diving into timezone arguments.
For example, 1717000000 is likely seconds. Passing it directly to new Date creates a date in January 1970 because JavaScript treats the number as milliseconds. The value 1717000000000 is likely milliseconds and lands near 2024. The numbers differ by a factor of 1000, but both can look realistic at a glance.
When the displayed date is wildly wrong, check unit first. Timezone mistakes usually shift a date by hours. Unit mistakes shift it by decades or centuries.
This quick digit check is especially helpful in support tickets where only a screenshot is available. If the raw value appears next to the formatted date, you can often identify the unit mismatch before asking for logs.
JavaScript Date Uses Milliseconds
The JavaScript Date constructor treats numeric values as milliseconds since the Unix epoch. That is convenient inside browser code, but it conflicts with APIs that use Unix seconds because seconds are common in logs, JWT claims, databases and command-line tools.
A good API wrapper should convert once at the boundary. If an API response says expires_at is seconds, convert it to milliseconds or a Date object in the data access layer, then keep the rest of the application consistent. Scattered multiplication by 1000 across components is how the same bug returns later.
The reverse bug also happens. Sending Date.now() to an API that expects seconds stores a value 1000 times too large. The backend may reject it, or worse, accept an expiration date far in the future.
UTC and Local Time Are a Separate Problem
After the unit is correct, then inspect timezone display. Unix timestamps represent an instant, not a local calendar label. Formatting that instant in UTC and formatting it in Asia/Shanghai, New York or another local timezone can produce different visible dates around midnight.
Do not try to fix a seconds/milliseconds bug by adding or subtracting timezone offsets. That mixes two different issues. First convert the unit correctly. Then choose the display timezone intentionally.
When reporting a bug, include both the raw timestamp and the formatted UTC result. That makes it easier for another developer to reproduce the exact conversion without guessing your local timezone.
JWT exp and iat Claims Are Usually Seconds
JWT claims such as exp, iat and nbf commonly use NumericDate, which is seconds since the epoch. Frontend code often reads those claims and compares them with Date.now(), which is milliseconds. That comparison is false unless one side is converted.
A token may appear expired immediately if milliseconds are compared against seconds. It may appear valid for a very long time if seconds are compared against milliseconds in the other direction. Both bugs can cause confusing login behavior that looks like an auth race condition.
The safest pattern is to name variables with units: expSeconds, nowMilliseconds, expiresAtDate. Boring names prevent expensive debugging.
Databases and Logs May Mix Units
Different stores use different conventions. PostgreSQL timestamp columns are not the same as Unix integer columns. Redis TTL commands can use seconds or milliseconds depending on the command. Observability systems may show ISO strings in the UI while exporting epoch milliseconds through an API.
When values pass through multiple systems, document the unit at each boundary. A cache job may read seconds from one API, write milliseconds to a queue and then log an ISO date. Each conversion is a place where a factor of 1000 can slip in.
If a log line shows both the raw number and the formatted date, keep them together. Separating them makes it harder to see whether the wrong value was stored or only formatted incorrectly.
TTL values deserve separate attention because they may represent duration rather than an absolute instant. A field named expires_at usually points to a moment in time, while ttl or max_age may be a number of seconds to add to the current time. Mixing timestamp and duration semantics can create bugs that look similar but need different fixes.
What to Check Next
Once the unit is fixed, test with a current timestamp, a past timestamp and a near-future expiration. Avoid using zero as the main test value because 0 can hide unit differences by always pointing at the epoch.
Use the Timestamp Converter to compare seconds and milliseconds side by side. If multiplying by 1000 makes the date reasonable, the bug is probably at a unit boundary. If the date is only off by hours, inspect timezone formatting instead.
Add a small assertion near the conversion code. For example, reject an expires_at value with 13 digits when the API contract says seconds. Defensive checks are especially useful when a field is supplied by external partners.
Code Examples
const expiresAtSeconds = 1717000000;
console.log(new Date(expiresAtSeconds).toISOString());
// Wrong: JavaScript treated seconds as milliseconds. function fromUnixSeconds(seconds) {
return new Date(seconds * 1000);
}
console.log(fromUnixSeconds(1717000000).toISOString()); const expSeconds = decodedToken.exp;
const nowSeconds = Math.floor(Date.now() / 1000);
const isExpired = expSeconds <= nowSeconds; Common Mistakes
- Passing Unix seconds directly to new Date().
- Sending Date.now() to an API that expects seconds.
- Debugging timezone before checking timestamp units.
- Using variable names like time or date without units.
- Testing only with 0 or tiny numbers that hide real-world behavior.
FAQ
How can I quickly tell seconds from milliseconds?
Modern Unix seconds are usually 10 digits, while JavaScript millisecond timestamps are usually 13 digits.
Does JavaScript Date use UTC?
A Date stores an instant internally. Numeric construction uses milliseconds since the Unix epoch; display methods can show UTC or local time.
Are JWT exp values seconds or milliseconds?
JWT NumericDate values such as exp are commonly seconds since the epoch, so compare them with seconds or convert deliberately.
Why is my date in 1970?
A common cause is passing a seconds timestamp into JavaScript APIs that expect milliseconds.