Three claims control when a token is valid, and all three are Unix timestamps in seconds, not milliseconds.
iat— issued atnbf— not before; reject until this instantexp— expires; reject at or after this instant
The millisecond bug
Date.now() in JavaScript returns milliseconds. Passing it directly to exp produces a token valid for roughly fifty thousand years.
// Wrong — a token valid until the year 54,000
const exp = Date.now() + 3600;
// Right
const exp = Math.floor(Date.now() / 1000) + 3600;This bug is invisible in testing, because the token works. It surfaces during an incident, when someone tries to work out why a token from last year still authenticates.
Choosing a lifetime
The lifetime is the window in which a stolen token is usable. A JWT cannot be revoked before exp unless you maintain a denylist — and if you maintain one, you have reintroduced the server-side state that stateless tokens were meant to avoid.
| Lifetime | Suitable for |
|---|---|
| 5–15 min | Access tokens. The standard, paired with a refresh token |
| 1 hour | Acceptable when refresh is not implemented |
| 24 hours | Only for low-value, read-only scopes |
| Over 24 hours | Effectively a permanent credential; use an opaque revocable key |
No exp | Never |
Clock skew
Servers disagree about the time. A token issued on a host running two seconds ahead can be rejected as "not yet valid" by a host running behind. Allow 60 seconds of skew on both nbf and exp when validating.
That allowance is also why very short lifetimes stop working: a 30-second token with 60 seconds of skew tolerance is accepted for longer after expiry than it was ever intended to be valid.
Refresh before failure
Clients should refresh proactively at around 75% of the lifetime, not wait for a 401 and retry. Waiting means every user hits a failed request at the moment of expiry, and any request that is not safe to retry — a payment, a write — becomes an error the user sees.
Frequently asked questions
What happens if a token has no exp?
It never expires. Every copy in every log, backup and browser cache stays valid forever. If you cannot rotate the signing key, you cannot invalidate it. Always set exp.
Should nbf and iat be the same value?
Usually yes. They differ only when you are deliberately issuing a token that becomes valid later — scheduled access, for example. Setting nbf slightly in the past is a common workaround for clock skew, but explicit skew tolerance in the validator is the cleaner fix.
How do I revoke a token before it expires?
You maintain a denylist keyed on jti, checked on every request. That is a database lookup per request, which is precisely the cost stateless tokens were supposed to remove — so keep lifetimes short instead, and use the denylist only for emergencies.
Are exp values in UTC?
Unix time has no timezone; it counts seconds since 1970-01-01T00:00:00Z. Any timezone confusion is in your formatting, not in the claim.