Common causes
1. An access token used after its short lifetime
Access tokens commonly live 5 to 60 minutes. Clients must refresh them, either before exp or after a 401, rather than reusing the same token for the whole session.
const res = await fetch('/api/orders', { headers: { Authorization: `Bearer ${token}` } });let res = await fetch('/api/orders', { headers: { Authorization: `Bearer ${token}` } });
if (res.status === 401) {
token = await refreshAccessToken();
res = await fetch('/api/orders', { headers: { Authorization: `Bearer ${token}` } });
}2. expiresIn given as a string without a unit
In the Node jsonwebtoken library a number means seconds, but a string without a unit means milliseconds: "3600" is 3.6 seconds. Environment variables are always strings, so convert them.
jwt.sign(claims, secret, { expiresIn: process.env.TOKEN_TTL });jwt.sign(claims, secret, { expiresIn: Number(process.env.TOKEN_TTL) });3. Clock skew between servers
If the verifier’s clock runs ahead of the issuer’s, fresh tokens look expired. Keep servers in sync with NTP and allow a small tolerance when verifying.
const payload = jwt.verify(token, secret);const payload = jwt.verify(token, secret, { clockTolerance: 30 });4. A token saved in a config file or CI secret
Tokens pasted into scripts, .env files or CI variables work until they expire and then fail with no other change. Fetch a token at runtime with a client-credentials flow instead.
API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjcyMjkyMDB9.c2lnbmF0dXJlAPI_CLIENT_ID=reporting-job
API_CLIENT_SECRET_FILE=/run/secrets/reporting-jobFrequently asked questions
How do I see when a token expires?
Decode it: the exp claim holds the expiry as a Unix timestamp. PasteKit converts exp, iat and nbf to readable UTC dates and shows how long ago or how far ahead each one is.
Should I just make tokens last longer?
Long-lived access tokens are risky because a leaked token stays usable. Keep access tokens short and use refresh tokens, which can be revoked, to get new ones.
Can I accept an expired token for one specific case?
Libraries offer an option such as ignoreExpiration, but it disables the check entirely. If you only need to read claims from an expired token, decode it without verifying, and never use that for authorisation.