Home / Tutorials / Authentication

Authentication · Authentication · Auth blocked

Fix: TokenExpiredError: jwt expired

Error TokenExpiredError: jwt expired

By the Erzon engineering team · A step-by-step fix, not a forum thread

The error TokenExpiredError: jwt expired means the JSON Web Token you presented is past the time set in its exp claim, so the verifier correctly refused it. This shows up most often in Node.js with the jsonwebtoken library, but the concept applies to every JWT implementation. The important framing before you touch any code: expiry is intended behavior. A token is supposed to stop working after a while. The correct fix is a working refresh flow, not disabling or extending expiry.

What this error means

Every JWT carries an exp claim, a Unix timestamp (in seconds) marking when the token stops being valid. When you call verify, the library compares exp against the current time. If exp is in the past, verification fails with TokenExpiredError and the request is rejected, usually surfacing as a 401. Nothing is broken in the token itself. It simply reached the end of its intended lifetime and the system did exactly what it was told to do.

Common causes

  • The access token expired and the client kept sending it because there is no refresh flow, or the refresh flow is broken.
  • Clock skew between the issuing server and the verifying server, so a still valid token looks expired on arrival.
  • The access token lifetime is too short for the actual use case, so tokens expire mid session.
  • A cached or stored token is reused after logout, a restart, or a long idle period.
  • The application never catches TokenExpiredError specifically, so it cannot trigger re-authentication.

How to fix it

Work in three phases: stabilize, diagnose, then fix.

Stabilize

  1. Confirm the impact. If users are locked out, verify that new logins succeed and only established sessions fail. That pattern points squarely at expiry or skew, not a signing key problem.
  2. Make sure your verify path returns a clean 401 on expiry instead of a 500. A 401 is the signal a client needs to refresh. Catch the specific error:
const jwt = require("jsonwebtoken");

function verifyAccessToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      const e = new Error("token expired");
      e.status = 401;
      e.code = "token_expired";
      throw e;
    }
    throw err;
  }
}

Diagnose

  1. Decode the token to read its claims. Do not trust the contents, just inspect them. Paste it into jwt.io, or base64 decode the payload segment locally:
echo "PASTE_JWT_HERE" | cut -d "." -f2 | base64 -d 2>/dev/null; echo
  1. Compare exp to the current server time in seconds:
const [, payloadB64] = token.split(".");
const payload = JSON.parse(Buffer.from(payloadB64, "base64").toString());
console.log("iat:", payload.iat, "exp:", payload.exp);
console.log("now:", Math.floor(Date.now() / 1000));

If exp is less than now, the token truly expired. If exp is in the future yet verify still fails, suspect clock skew.

  1. Check server clocks. On Linux, timedatectl shows whether NTP sync is active. A verifier whose clock runs ahead will reject fresh tokens.

Fix

  1. Implement or repair the refresh flow. On a 401 with an expired access token, use the refresh token to obtain a new access token and retry the original request once:
async function requestWithRefresh(doRequest) {
  let res = await doRequest();
  if (res.status === 401) {
    const refreshed = await fetch("/auth/refresh", { method: "POST", credentials: "include" });
    if (!refreshed.ok) throw new Error("refresh failed, re-login required");
    res = await doRequest();
  }
  return res;
}
  1. Add a small clockTolerance to absorb minor drift between servers. Keep it to a few seconds:
jwt.verify(token, process.env.JWT_SECRET, { clockTolerance: 5 });
  1. Set sensible lifetimes: short access tokens (for example expiresIn: "15m") paired with longer lived refresh tokens. Do not stretch access token expiry to avoid the refresh work.

How to prevent it

  • Use short access tokens plus refresh tokens, and refresh silently before the access token expires.
  • Handle 401 responses everywhere with a single retry after refresh, so expiry never reaches the user.
  • Run NTP on every server that issues or verifies tokens.
  • Clear stored tokens on logout and on app restart so stale ones are never replayed.
  • Do not extend token lifetime as a workaround. It only widens the window a leaked token stays valid.

When to call a senior engineer

If the refresh flow is racing under load, if tokens intermittently fail across a cluster where some nodes drift, or if you are unsure whether the failure is expiry, skew, or a rotated signing key, it is worth a second pair of eyes. These issues are usually quick to isolate once someone reads the claims and the server clocks together, and getting the refresh logic right the first time avoids a second outage.

Related errors we fix

Still down after this?

Want a senior engineer to just fix it?

Paste this exact error into the form. A senior engineer replies within one business hour with what is likely wrong and a flat quote. Triage is free.

Book a fix

← Back to all error fixes

Still down?

Get a senior engineer on it now

Leave your email and a line about what is happening. A senior engineer replies within one business hour with what is likely wrong and a flat quote. Triage is free.

Or email [email protected]. Same engineers, same one-hour clock.