JWT expired: TokenExpiredError and "Signature has expired"

A JWT’s exp claim is the time, in seconds since 1970, after which it must be rejected. The verifier compared it with its own clock and found the token too old. The signature can be fine; the token has simply run out. PasteKit decodes the claims, shows the issue and expiry times in UTC with how long ago they were, and flags the expiry as a warning, as it does for this sample, which expired on 1 January 2026.

Seen as:

  • TokenExpiredError: jwt expired
  • jwt.exceptions.ExpiredSignatureError: Signature has expired
  • JWTExpired: "exp" claim timestamp check failed
  • IDX10223: Lifetime validation failed. The token is expired.

Input

Settings

History

Load from URL

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.

Before
const res = await fetch('/api/orders', { headers: { Authorization: `Bearer ${token}` } });
After
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.

Before
jwt.sign(claims, secret, { expiresIn: process.env.TOKEN_TTL });
After
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.

Before
const payload = jwt.verify(token, secret);
After
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.

Before
API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjcyMjkyMDB9.c2lnbmF0dXJl
After
API_CLIENT_ID=reporting-job
API_CLIENT_SECRET_FILE=/run/secrets/reporting-job

Frequently 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.

Related