Almost every API and auth system eventually reduces time to a number: seconds or milliseconds since 1970-01-01T00:00:00Z. That number is easy to store and compare — and easy to misread. Mixing seconds with milliseconds, ignoring timezone offsets, or trusting local clock display during DST transitions produces "random" 401s, scheduling bugs, and off-by-a-factor-of-1000 errors that look mysterious until you inspect the raw value.
Bottom line: Know whether your epoch is seconds or milliseconds before you convert. Store and exchange UTC (or explicit offsets). Treat JWT exp as Unix seconds. Convert with tools that show both human and machine forms so you catch unit mistakes early.
Who this guide is for
Backend and frontend engineers debugging expired tokens, webhook timestamps, cron schedules, analytics events, and database timestamptz values. If you have ever multiplied or divided by 1000 "just in case," this article replaces guesswork with a checklist.
Unix time in one paragraph
Unix time (epoch time) counts seconds since the Unix epoch in UTC. It ignores leap seconds in most practical systems (POSIX time). JavaScript's Date.now() returns milliseconds. Many databases and JWT libraries use seconds. That single mismatch causes most timestamp bugs.
| Source |
Typical unit |
Example (approx. mid-2026) |
JWT exp, iat, nbf |
seconds |
1784000000 |
Python time.time() |
seconds (float) |
1784000000.123 |
Date.now() / Date#getTime() |
milliseconds |
1784000000123 |
Unix date +%s |
seconds |
1784000000 |
| Some Java APIs / MongoDB |
milliseconds |
1784000000123 |
Rule: 10-digit values near "now" are usually seconds; 13-digit values are usually milliseconds. Confirm with the Unix timestamp converter instead of guessing.
Seconds vs milliseconds: conversion rules
// Seconds → Date
const fromSeconds = new Date(epochSeconds * 1000);
// Milliseconds → Date
const fromMillis = new Date(epochMillis);
// Date → seconds (JWT-style)
const exp = Math.floor(Date.now() / 1000);
| Mistake |
Symptom |
Fix |
| Treat ms as seconds |
Date in year ~50,000+ |
Divide by 1000 |
| Treat seconds as ms |
Date in 1970 |
Multiply by 1000 |
| Truncate instead of floor |
Off-by-one second near boundaries |
Use Math.floor for exp |
| Float seconds to int badly |
Early expiry |
Be explicit about rounding policy |
JWT exp, iat, and nbf
Per RFC 7519, registered time claims are NumericDate values: seconds since epoch.
{
"sub": "usr_8f2a",
"iat": 1784000000,
"nbf": 1784000000,
"exp": 1784003600
}
Debugging workflow:
- Copy the token and open the JWT decoder (client-side).
- Read
exp as Unix seconds.
- Convert with the Unix timestamp tool to a UTC wall time.
- Compare against "now" in UTC — not only your laptop's local clock display.
- Check clock skew between auth server and API (see below).
A token that "just issued" but already fails exp checks often means the verifier used milliseconds, or one machine's clock is wrong by minutes.
ISO-8601 (and the Internet profile in RFC 3339) is what APIs should put in JSON for humans and logs:
2026-07-11T14:30:00Z
2026-07-11T14:30:00.123Z
2026-07-11T10:30:00-04:00
| Form |
Meaning |
Z |
UTC (Zulu) |
±HH:MM |
Offset from UTC |
| Fractional seconds |
Precision beyond whole seconds |
Date-only 2026-07-11 |
Calendar date — not a timestamp (timezone ambiguous) |
Parsing pitfalls in JavaScript
// Prefer explicit UTC
Date.parse("2026-07-11T14:30:00Z"); // fine
// Date-only forms are interpreted as UTC in ES5+ for ISO, but
// "2026-07-11" vs "07/11/2026" behavior differs across engines — avoid locale forms
Date.parse("07/11/2026"); // DO NOT rely on this
Always include time and offset (or Z) for instants. Use date-only strings only for civil dates (birthdays, fiscal days) with a documented timezone policy.
Timezones and DST
Unix timestamps and UTC instants do not observe DST. DST affects local display and local scheduling rules.
| Scenario |
Risk |
| Store "09:00 America/New_York" as a bare timestamp without zone |
Ambiguous on fall-back day (1:30 AM happens twice) |
| Add 24 hours in local time across DST spring-forward |
Landing on wrong civil time |
| Compare server UTC logs to laptop local screenshots |
Apparent "skew" that is only offset |
| Cron in container TZ ≠ intended business TZ |
Jobs fire an hour off after DST change |
Conversion workflow:
- Keep the source of truth as UTC instant (epoch or
...Z).
- Convert for display with an explicit IANA zone (
America/New_York, Europe/Berlin) via the Timezone converter.
- Never serialize "local time without offset" across systems.
// Display in a zone (modern platforms)
new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
dateStyle: "medium",
timeStyle: "long",
}).format(new Date(1784000000 * 1000));
Clock skew and verification windows
Distributed systems disagree about "now" by tens of milliseconds to several minutes. Auth servers often allow a small leeway (e.g. 60 seconds) when checking exp and nbf.
| Check |
Guidance |
| API rejects fresh tokens |
Compare API host NTP vs auth host; inspect iat/exp |
| Tokens valid "in the future" |
Client clock ahead; fix NTP or allow nbf leeway carefully |
| Browser vs server mismatch |
Do not trust Date on the client for security decisions |
Security checks belong on the server with synchronized clocks. Client-side conversion is for debugging and UX.
Safe conversion playbook
When a timestamp looks wrong:
- Count digits — 10 ≈ seconds, 13 ≈ ms (for contemporary dates).
- Convert both ways in the Unix timestamp tool — if the human date is nonsensical, you have the wrong unit.
- Normalize to UTC before comparing across services.
- Map to local zones only for UI, using Timezone converter.
- For JWTs, decode claims with JWT decoder, then convert
exp/iat as seconds.
Quick reference snippets
# Current Unix seconds
date +%s
# Seconds → UTC
date -u -d @1784000000
# Node
node -e "console.log(new Date(1784000000*1000).toISOString())"
-- PostgreSQL: seconds → timestamptz
SELECT to_timestamp(1784000000);
-- milliseconds
SELECT to_timestamp(1784000000123 / 1000.0);
API contract recommendations
- Document the unit next to every epoch field:
"expiresAt": 1784003600 // Unix seconds.
- Prefer ISO-8601 strings in public JSON unless you need compact numeric fields.
- Reject ambiguous date-only strings for absolute deadlines.
- Include
timezone or store UTC-only for events.
- In tests, freeze time (
jest.useFakeTimers, sinon.useFakeTimers) instead of asserting against Date.now().
Privacy note for online converters
Timestamps themselves are rarely secret, but values pasted alongside tokens, order IDs, or session payloads can be. Prefer client-side converters that do not upload your clipboard. codefunc's Unix timestamp, Timezone converter, and JWT decoder run in the browser — useful when the surrounding payload is sensitive.
Practical takeaway
Decide seconds vs milliseconds first; everything else is display. Use ISO-8601 with Z or an explicit offset for APIs. Treat JWT time claims as Unix seconds and verify them against UTC. Convert zones only at the edges with IANA names, and watch DST when scheduling civil times. When debugging, paste into the Unix timestamp and Timezone converter tools — and decode tokens with the JWT decoder — before changing production expiry settings.
Sources