iKit
Technical · 10 min read ·

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 (2026)

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, and iat are Unix timestamps in seconds (RFC 7519 NumericDate), not milliseconds.
  • A 2026-era exp is 10 digits; Date.now() in JavaScript is 13 — multiply exp by 1000.
  • A token is valid while nbf ≤ now < exp; iat tells you its age.
  • Check expiry by hand: exp * 1000 < Date.now() means expired.
  • Validation failing on a future exp is 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

Related on iKit

Related posts