Skip to content
ProfitTool
Back to all articles
Developer Tools
6 min read

JWT Expiration Explained: exp, iat and nbf Claims

How JWT time claims work — exp, iat and nbf — plus clock skew tolerance, choosing a token lifetime, refresh-token rotation and the revocation gap.

ProfitTool Team

Security & Dev

The Three Time Claims

RFC 7519 defines three time-related claims. All three are NumericDate values — seconds since the Unix epoch, in UTC, ignoring leap seconds.

ClaimNameRule
expExpiration TimeThe token MUST NOT be accepted on or after this time
nbfNot BeforeThe token MUST NOT be accepted before this time
iatIssued AtWhen the token was minted; used to judge its age
{
  "sub": "12345",
  "iat": 1769000000,
  "nbf": 1769000000,
  "exp": 1769003600
}

exp − iat is the token's lifetime — here 3600 seconds, one hour.

Two details do most of the damage in practice. First, seconds, not milliseconds: Date.now() in JavaScript returns milliseconds, and passing it straight into exp yields a token that expires around the year 58,000. Use Math.floor(Date.now() / 1000). Second, exp is not optional in any meaningful sense — the specification calls it optional, but a token without one never expires, and a leaked token without one is a permanent credential.

What Expiration Is Actually For

A JWT is verified by checking a signature, not by looking anything up. That is the whole point of the format — and it means the issuer has no way to reach out and cancel a token it has already handed over.

Expiration is the only revocation mechanism a plain JWT has. exp is not a session-length preference; it is the upper bound on how long a stolen token remains useful. Every decision about token lifetime is really a decision about that exposure window.

Clock Skew and Leeway

Two servers rarely agree on the time to the second. If the issuer's clock runs 30 seconds ahead of the verifier's, a token minted with nbf set to "now" is rejected as not-yet-valid for the first 30 seconds of its life.

RFC 7519 anticipates this: implementers may allow a small leeway, "usually no more than a few minutes," when checking exp and nbf.

// Node — jsonwebtoken
jwt.verify(token, key, { algorithms: ["RS256"], clockTolerance: 30 });   // seconds
# Python — PyJWT
jwt.decode(token, key, algorithms=["RS256"], leeway=30)

Keep leeway small — 30 to 60 seconds. It is a tolerance for imperfect clocks, not a way to extend token life, and every second of leeway is a second of extra validity for an expired token. The real fix is NTP on every host; leeway covers the residual drift.

Choosing a Lifetime

The two forces pull in opposite directions. Short tokens limit the damage from a leak but force frequent refreshes. Long tokens are convenient and dangerous.

Token typeTypical lifetimeReasoning
Access token (browser or mobile app)5–15 minutesShort enough that a leaked token expires before most attacks are mounted
Access token (service-to-service)5–60 minutesSame logic; the higher end is tolerable inside a trusted network
Refresh tokenDays to weeksLong-lived, but stored more carefully and revocable server-side
ID token (OIDC)5–60 minutesConsumed once at sign-in; length rarely matters
Email verification / password reset15 minutes – 24 hoursSingle use, and should be invalidated on use as well as by exp

The pattern worth internalising: short access token, long refresh token. The access token travels on every request and is exposed constantly; the refresh token is sent only to the token endpoint, stored more defensively, and can be revoked because the authorisation server keeps a record of it.

An hour-long access token is not a compromise between the two — it is a long exposure window with no revocation. If you want longer sessions, lengthen the refresh token, not the access token.

Refresh Tokens and Rotation

The refresh flow:

  1. Sign-in issues a short access token and a long refresh token.
  2. The client uses the access token until it expires.
  3. On a 401, the client posts the refresh token to the token endpoint.
  4. The server validates it against stored state and issues a new access token.

Rotation adds one rule: step 4 also issues a new refresh token and invalidates the old one. That converts a stolen refresh token from an open-ended credential into a single-use one, and — more usefully — makes theft detectable.

If a rotated refresh token is presented twice, one of the two presenters is an attacker, and the server cannot tell which. The correct response is to revoke the entire token family and force re-authentication. This reuse detection is what the IETF's OAuth 2.0 Security Best Current Practice recommends for clients that cannot hold a secret — browser and mobile apps in particular.

Rotation trades statelessness for detectability: the server must now store refresh-token state. That is an acceptable trade, because refresh tokens are presented rarely, so the lookup cost is negligible compared with doing it on every API call.

The Revocation Gap

Between "you revoked access" and "the token expires" there is a window in which a valid signature still opens the door. Demote a user from admin at 10:00:00 and a 15-minute access token minted at 09:59:00 keeps asserting "role": "admin" until 10:14:00.

There is no way to close that gap without server-side state. The options, in increasing cost:

ApproachHow it worksCost
Short expiry onlyAccept the window; keep it smallNone — the window remains
jti denylistStore revoked token IDs until their exp passesOne cache lookup per request
Token version counterEmbed a version claim; compare against the user recordOne lookup per request
Introspection endpointVerifier asks the issuer about every tokenA network call per request — this is a session store

Choose deliberately. A denylist on every request means you are paying session-store costs while keeping JWT complexity — sometimes correct for a high-value subset of actions, rarely correct across the board. A common middle path is to skip the denylist for ordinary reads and require a fresh token for privileged operations.

Debugging "Token Expired"

SymptomLikely causeCheck
Every token rejected immediatelyexp written in milliseconds, or the server clock is wrongDecode the token; a current exp is ten digits, not thirteen
Rejected for the first ~30 seconds, then fineClock skew against nbfSync clocks with NTP; add small clockTolerance
Works locally, fails in CI or containersContainer clock drift, or a fixed clock in a test fixtureCompare date +%s on both hosts
Expires far sooner than configuredLibrary default overriding your value, or expiresIn set in the wrong unitDecode a freshly issued token and compute exp − iat
Intermittent failures under loadSome instances have driftedCheck clock sync across the whole fleet, not one host
Valid token rejected after a deploySigning key rotated without a kid-based overlapKeep the previous verification key active until old tokens expire

The first step is always the same: decode the token and read iat and exp as real dates. Most reports of "the token expires too fast" turn out to be a unit error rather than a configuration one.

Common Mistakes

  • No exp at all. Creates a permanent credential out of something designed to be temporary.
  • Milliseconds in a NumericDate field. Silently produces tokens that never expire.
  • Long access tokens "for convenience." The convenience belongs in the refresh token.
  • Refresh tokens with no server-side state. A stateless refresh token cannot be revoked, which defeats its purpose entirely.
  • Large leeway to paper over clock problems. Fix the clocks.
  • Refreshing only on a 401. A request that fails mid-flow after a long idle period is a poor user experience; refresh proactively shortly before expiry.
  • Assuming timezone matters. NumericDate is always UTC — a timezone mismatch is never the cause. A wrongly set clock is.

Frequently Asked Questions

What happens at the exact moment of expiry? The specification says a token must not be accepted on or after exp. Equality counts as expired.

Can I extend an existing token's expiry? No. Changing any claim invalidates the signature. Issue a new token.

Is a very long exp acceptable if the token is stored securely? Storage reduces the chance of a leak but not the consequence. Lifetime bounds the consequence, and it is the part you control absolutely.

Should refresh tokens be JWTs? Usually not. Refresh tokens need server-side state anyway, so an opaque random string is simpler and reveals nothing.

Do exp and nbf need to be present together? No. exp should effectively always be set; nbf is only useful for tokens intended to become valid later, which is uncommon.

Inspect exp, nbf and iat with live countdowns and human-readable dates using the JWT Claims Inspector, or decode a whole token with the JWT Decoder. To convert a raw NumericDate by hand, use the Unix Timestamp Converter. For the format itself see What Is a JWT?, and for reading a token segment by segment, How to Decode a JWT.

Tags:#jwt#authentication#exp claim#token expiry

ProfitTool Team

Security & Dev

Written and maintained by the ProfitTool team. Where an article references a ProfitTool utility, the behaviour described is the behaviour of the tool on this site — you can open it and check the output yourself.

Try the Interactive Tools Mentioned

Free to use with no ProfitTool signup. Data handling varies by tool.