Encodings / JWT

JWT expired error

The token's exp claim is past the current time and the verifier is correctly rejecting it. The fix depends on which clock is wrong and why the token was issued so short.

Decode a token to see its exp →

What the verifier saw

Standard libraries report this differently. Common forms:

JsonWebTokenError: jwt expired
ExpiredSignatureError: Signature has expired
TokenExpiredError: TokenExpiredError: jwt expired
401 Unauthorized — token expired

Underneath, all of them ran the same check:

now >= payload.exp + leeway

Three knobs: now (the verifier's clock), payload.exp (set by the issuer), and leeway (a tolerance for skew, often 0 by default).

The five causes, ranked by frequency

1. The token genuinely expired

Most common, least surprising. Your access token's TTL elapsed between issuance and use. Refresh it.

// Pseudo
if (response.status === 401) {
  const fresh = await refresh(refreshToken);
  return retry(request, fresh.accessToken);
}

Build this once, at the HTTP-client layer, not per-call. If you find yourself catching expired-token errors in business logic, the abstraction is wrong.

2. Clock skew between issuer and verifier

If the verifier's clock is 30 seconds ahead, a token issued with a 30-second lifetime is dead on arrival. If the verifier's clock is behind, you'll see "not yet valid" (nbf) errors instead.

Fix: enable leeway in the verifier (most libraries support it), and run NTP / chrony on every host.

// node — jsonwebtoken
jwt.verify(token, secret, { clockTolerance: 30 });

// python — pyjwt
jwt.decode(token, secret, leeway=30, algorithms=["HS256"])

// go — jwt-go
parser := jwt.NewParser(jwt.WithLeeway(30 * time.Second))

3. Wrong timezone in the issuer

exp is Unix seconds — UTC, no timezone. If your issuer computes it from local time and converts wrong, every token is shifted by your offset (e.g., -7 hours in PDT). Symptom: tokens fail consistently by exactly your timezone offset.

Fix: always use time.time(), Date.now() / 1000, time.Now().Unix() — never the local-time variant.

4. exp is in milliseconds, not seconds

JWT spec is unambiguous: seconds. JavaScript's Date.now() returns milliseconds. Forgetting to divide by 1000 produces a token that "expires in 1971" (1.7 trillion seconds past epoch). Verifiers treat that as expired by ~50 years.

// Wrong
const exp = Date.now() + 3600 * 1000;

// Right
const exp = Math.floor(Date.now() / 1000) + 3600;

5. Cached token in the client

A service worker, localStorage, or in-memory copy holds an old token after a logout/refresh cycle. The user re-authenticated; the cache didn't update. Hard-refresh, clear storage, or invalidate the cache key on logout.

Detection: paste your token

Our JWT decoder reads the exp claim and shows "expired N min ago" or "expires in N min" inline. Runs in your browser; the token never leaves the page.

Defensive defaults

  1. Access token TTL: 5–15 minutes. Refresh token TTL: 1–30 days, rotated on use.
  2. Verifier leeway: 30–60 seconds. Smaller and skew bites; larger and revoked tokens stay valid too long.
  3. One refresh path, at the HTTP layer. No retry storms — if refresh fails, surface to the user.
  4. NTP on every host. JWT problems are almost always clock problems in disguise.

Related

FAQ

How do I check if my JWT is expired without verifying?

Decode the payload (Base64 URL of the second segment), parse as JSON, and compare the 'exp' field (Unix seconds) to the current time. Our /decode-jwt page does this in your browser and surfaces the time-to-expiration directly.

What's the difference between exp, nbf, and iat?

All three are Unix timestamps in seconds. 'exp' is when the token stops being valid. 'nbf' (not before) is when it starts being valid; verifiers reject tokens used earlier. 'iat' (issued at) is when it was created — informational, used for auditing, not directly checked.

Why does my token expire 'in the future' according to the verifier?

Server clock skew. Two machines whose clocks disagree by 60 seconds will see a fresh token as either pre-issued or expired depending on direction. Configure leeway (typically 30–60s) in your verifier and run NTP on every host.

Should I just extend exp to a year and forget about it?

No. Long-lived access tokens are the dominant cause of stolen-token incidents. The pattern is short-lived access (5–15 min) plus a longer-lived refresh token rotated on use. The refresh token sits in an httpOnly cookie or secure store and never reaches client JS.

Can I refresh a token after it's expired?

Not directly — the access token is dead. You exchange the refresh token (which has its own, longer expiration) for a new access token. If the refresh token is also expired or revoked, the user must re-authenticate.

Open the JWT decoder →