How to Read JWT exp and iat as Unix Timestamps (2026)
JWT exp, nbf, and iat are Unix timestamps in seconds. Learn to read them in 3 seconds, convert them to a date, and check token expiry by hand.
How to Read JWT exp and iat as Unix Timestamps
Every JWT carries its lifetime in plain numbers, but they look cryptic: "exp": 1700003600. That is not random — it is a Unix timestamp, the count of seconds since 1970. Once you know that, you can read a token's expiry, issue time, and not-before window at a glance, without a library and without pasting the token into someone else's server. This guide shows you how to read JWT exp, iat, and nbf in seconds.
TL;DR
- JWT
exp,nbf, andiatare Unix timestamps in seconds (RFC 7519 NumericDate), not milliseconds. - A 2026-era
expis 10 digits;Date.now()in JavaScript is 13 — multiplyexpby 1000. - A token is valid while
nbf≤ now <exp;iattells you its age. - Check expiry by hand:
exp * 1000 < Date.now()means expired. - Validation failing on a future
expis usually clock skew — allow ~60s leeway.
What JWT exp, iat, and nbf Actually Contain
The payload (the middle section) of a JWT is just Base64URL-encoded JSON. Decode it and you get a flat object of claims. Three of the standard claims describe time, and all three share one format.
NumericDate is just Unix seconds
Per RFC 7519, the time claims use a type called NumericDate: a JSON number giving the seconds elapsed since 1970-01-01T00:00:00Z UTC, ignoring leap seconds. That is the same "Seconds Since the Epoch" definition POSIX uses, where every day counts as exactly 86,400 seconds. In other words, an exp value is a plain Unix timestamp — the identical kind of number you'd paste into the iKit Unix Timestamp Converter to turn into a human date.
A decoded payload looks like this:
{
"sub": "1234567890",
"name": "Ada",
"iat": 1700000000,
"nbf": 1700000000,
"exp": 1700003600
}
Here iat is 2023-11-14 22:13:20 UTC, and exp is exactly one hour later (1700000000 + 3600).
The three time claims at a glance
| Claim | Meaning | Value format |
|---|---|---|
iat |
Issued At — when the token was created | Unix seconds |
nbf |
Not Before — earliest valid moment | Unix seconds |
exp |
Expiration — token stops being valid | Unix seconds |
RFC 7519 marks all three as OPTIONAL, so a token may carry any subset. Most real tokens include at least iat and exp.
Why they are seconds, not milliseconds
This trips up almost everyone who works in JavaScript. The language's native clock — Date.now() — returns milliseconds, so it is a 13-digit number today. JWT timestamps are seconds, so they're 10 digits. They describe the same instant, just at different resolution. We covered this 1000× gap in detail in why your JavaScript timestamp is bigger than your backend's; for JWTs the rule is simply: the number in the token is seconds.
How to read a JWT exp claim in 3 seconds
You don't need to run code to know when a token dies. Three steps.
Decode the payload
A JWT is header.payload.signature, each part Base64URL. Split on the dots, take the middle part, and Base64URL-decode it. iKit's JWT Decoder does this in the browser without sending the token anywhere, or you can decode the middle segment with the Base64 tool if you want to see the raw step.
Read exp by eye
Look at the exp number. The leading digits already tell you the era: in 2026, any current timestamp starts with 17…. A value like 1700003600 is clearly a real seconds-based timestamp; a value like 1700003600000 is milliseconds (someone set it wrong). Paste the seconds value into a converter to get the wall-clock date.
Spot an expired token
Compare exp to the current Unix time. If exp is smaller than "now in seconds," the token is dead. You can grab the current epoch from the converter's live clock, or in a terminal with date +%s. No signature math required to answer "is this expired?" — though, as we'll stress below, expired and valid are two different questions.
How to convert a JWT timestamp to a date in JavaScript
When you do want code, the conversion is one multiplication away.
Turn exp into a Date object
JavaScript's Date constructor expects milliseconds, and getTime() returns milliseconds, so multiply the seconds value by 1000:
const exp = 1700003600; // from the JWT
const when = new Date(exp * 1000);
console.log(when.toISOString());
// "2023-11-14T23:13:20.000Z"
Forgetting the * 1000 is the single most common JWT-time bug — you get a date in January 1970 because you passed seconds where milliseconds were expected.
A one-line expiry check
To test expiry, compare against Date.now(), which is also in milliseconds:
const isExpired = exp * 1000 < Date.now();
If you'd rather work entirely in seconds, divide instead:
exp < Math.floor(Date.now() / 1000).
The same check in bash
On any Unix shell, date +%s prints the current epoch in seconds, so you can compare directly without converting units:
EXP=1700003600
NOW=$(date +%s)
[ "$EXP" -lt "$NOW" ] && echo "expired" || echo "still valid"
This is handy in CI scripts that gate on a token's freshness before running an integration test.
Why is my JWT exp 10 digits and my iat the same length
If exp and iat look like the same kind of number, that's because they are — both are NumericDate seconds. The length only changes with the era, not the claim.
Seconds, milliseconds, and digit count
A quick reference for what a number's length is telling you in 2026:
| Digits | Unit | Example |
|---|---|---|
| 10 | seconds (JWT) | 1700003600 |
| 13 | milliseconds (JS) | 1700003600000 |
| 16 | microseconds | 1700003600000000 |
If a JWT field has 13 digits, the issuer made a mistake — the spec requires seconds. Validators that assume seconds will treat a millisecond exp as a date roughly 50,000 years in the future, so the token effectively never expires.
Clock skew and leeway
A token can have a perfectly future exp and still be rejected, because the two machines disagree about "now." RFC 7519 anticipates this: implementers MAY allow some small leeway, usually no more than a few minutes, to absorb clock skew. In practice a 60-second window is the common convention (OpenID Connect Core uses it). If you see intermittent "token expired" or "token not yet valid" errors, check that both clocks are synced with NTP before blaming the code.
nbf and the not-yet-valid window
nbf (not before) is the mirror image of exp. The token MUST NOT be accepted before this instant, so the usable window is nbf ≤ now < exp. A common cause of "this brand-new token doesn't work" is an nbf set to issue time on a server whose clock runs a few seconds ahead of the verifier — the same leeway fixes it. For a claim-by-claim tour beyond timing, see our field guide to standard JWT claims.
Common mistakes reading JWT timestamps
A few traps catch even experienced developers.
Multiplying by the wrong factor
Going from a JWT timestamp to a Date needs × 1000; going from Date.now() to a JWT-style value needs ÷ 1000 and a floor. Mixing these up produces dates in 1970 or in the year 50,000. When in doubt, paste the raw number into the converter and read the date back — if it isn't near today, your factor is wrong.
Trusting exp without verifying the signature
Reading exp tells you whether a token claims to be unexpired. It does not tell you the token is authentic. Anyone can craft a JWT with a far-future exp; only signature verification proves the issuer set it. Decoding is for inspection and debugging — never make an authorization decision on a decoded-but-unverified payload. (This is exactly how the alg: none attack sneaks past naive code.)
Timezone confusion
NumericDate is always UTC — there is no timezone in the number itself. When you convert, decide explicitly whether you want UTC or local time. toISOString() gives you UTC with a Z; toString() gives the browser's local zone. Logs that mix the two cause hours of phantom "expired early" debugging, the same way raw Unix timestamps in logs do.
References
- RFC 7519 — JSON Web Token (JWT) — primary spec; cited for the NumericDate definition and the exp/nbf/iat claims plus the clock-skew leeway note.
- MDN: Date.prototype.getTime() — confirms JavaScript time values are milliseconds since the epoch.
- MDN: Date.now() — current epoch in milliseconds, used in the expiry-check snippets.
- OpenID Connect Core 1.0 — source for the widely-used 60-second clock-skew convention.
Related on iKit
- How JWT exp, nbf, and iat control token expiry — the validation-side companion to this post, covering how verifiers enforce these same claims.
- Unix Timestamp Explained: 10-Digit Numbers in Your Logs — what those 10-digit seconds values mean wherever they appear, JWT or not.
- Why Your JavaScript Timestamp Is 1000× Bigger Than Your Backend's — the seconds-vs-milliseconds gap that causes most JWT date bugs.
- Convert Unix Timestamp to Date Without Timezone Bugs — turning an exp value into the right wall-clock time across zones.
- Epoch Time Cheat Sheet: Seconds, Millis, Micros, Nanos — the digit-count reference for telling units apart at a glance.
- The Year 2038 Problem: Who's Affected and How to Test — what happens when 32-bit seconds counters overflow, JWT exp included.
Related posts
Why Your Converted JPG Is Bigger Than the PNG (2026)
Converted a PNG to JPG and the file grew? Here's why JPEG bloats flat graphics and screenshots, and the quick fix that actually shrinks the image.
What Was Unix Timestamp 1700000000? Date Forensics (2026)
Unix timestamp 1700000000 was Tuesday, November 14, 2023, 22:13:20 UTC. Learn how to date any timestamp from its leading digits, no converter needed.
The Year 2038 Problem: Who's Affected and How to Test (2026)
The Year 2038 problem overflows 32-bit Unix time on 19 Jan 2038. Here's who is still affected in 2026 and how to test your own systems for it.