TokenExpiredError: jwt expired
Node.js · updated 2026-11-11
Thrown by jsonwebtoken's jwt.verify() when the token's exp claim is in the past. The error carries expiredAt. Two siblings come from the same function: JsonWebTokenError "invalid signature" (wrong secret or key) and JsonWebTokenError "jwt malformed" (not a JWT at all, often an empty or "undefined" string).
TokenExpiredError: jwt expired
at node_modules/jsonwebtoken/verify.js:190:21
at getSecret (node_modules/jsonwebtoken/verify.js:97:14)
at module.exports [as verify] (node_modules/jsonwebtoken/verify.js:101:10)
at requireUser (src/middleware/auth.ts:12:23)
expiredAt: 2026-11-11T07:39:51.000ZQuick fix
An expired token is an expected event, not a server bug: catch TokenExpiredError in your auth middleware and answer 401 so the client refreshes or signs in again. If tokens fail right after they are issued, compare clocks between the server that signs and the one that verifies, or allow a few seconds with clockTolerance.
- Catch the error in the auth middleware and return 401 with a clear code (for example token_expired) instead of a 500.
- On the client, refresh the access token when it gets that 401, then retry the request once.
- Compare expiredAt in the error with the current time: hours ago means an old token is being reused; seconds means a short lifetime or clock skew.
- Clock skew between machines: allow it with jwt.verify(token, key, { clockTolerance: 5 }).
- Don't raise the lifetime to hide the problem; pair short access tokens with a refresh token.
import jwt from 'jsonwebtoken';
export function requireUser(req, res, next) {
const token = req.headers.authorization?.replace(/^Bearer /, '');
try {
req.user = jwt.verify(token ?? '', process.env.JWT_SECRET!, { clockTolerance: 5 });
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) return res.status(401).json({ error: 'token_expired' });
return res.status(401).json({ error: 'invalid_token' });
}
}Paste this into Claude Code, Cursor or Codex in the project that throws it. The agent reads this guide in plain text and applies the fix that matches your stack.
Read https://runheed.com/errors/jwt-expired.md and fix this error in this project.Why it happens
The client kept an old token
A tab, mobile app or script reuses an access token after its exp without refreshing it.
Very short lifetime
expiresIn set to seconds or minutes, with no refresh flow, so long sessions hit it constantly.
Clock skew
The verifying server's clock runs ahead of the signing server's, so fresh tokens look expired.
Tokens stored somewhere long-lived
A token saved in a cookie, a database or an env var outlives its exp and gets sent again.
Other fixes
- Count these but don't page on them: a steady rate is normal. A spike after a deploy usually means a changed secret, key or lifetime.
- "invalid signature" after rotating a secret means old tokens are still in use; accept both keys for one token lifetime.
Same error, other wording
- JsonWebTokenError: invalid signature
- JsonWebTokenError: jwt malformed
- jwt.exceptions.ExpiredSignatureError: Signature has expired
Let your coding agent catch the next one
Runheed records this error in production with the stack trace, the breadcrumbs before it and, when you set it, the agent session that shipped the code, and serves it to Claude Code, Cursor or any MCP client. With it connected, you can ask:
> Find the jwt expired and invalid signature errors, tell me whether they are normal expirations, clock skew or a key change, and make the auth middleware answer 401 with a refresh path instead of 500.
Setup for Node.js and Express or Vercel Functions, then have your agent fix and resolve it.
Related errors
- [UnhandledPromiseRejection: This error originated either by throwing inside of an async...
- RangeError: Invalid time value
- No signatures found matching the expected signature for payload. Are you passing the raw...
More in Network and HTTP
- Error: read ECONNRESETNode.js
- Error: connect ECONNREFUSED 127.0.0.1:5432Node.js
- Error: connect ETIMEDOUT 10.0.4.12:5432Node.js
- TypeError: fetch failedNode.js
- Error: self-signed certificate in certificate chainNode.js
- Error: getaddrinfo ENOTFOUND api.example.comNode.js
- Error: write EPIPENode.js
- Access to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com'...Node.js
- AxiosError: Request failed with status code 401Node.js
- FunctionsHttpError: Edge Function returned a non-2xx status codeSupabase
- requests.exceptions.ConnectionError: HTTPSConnectionPool(host='api.example.com'...Python
- httpx.ReadTimeout: The read operation timed outPython
- ConnectionResetError: [Errno 104] Connection reset by peerPython
- BrokenPipeError: [Errno 32] Broken pipePython
- [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer...Python
- Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the clientNode.js
- 422 Unprocessable Entity: {"detail":[{"type":"missing","loc":["body","email"],"msg":"Field...Python
- Error: 429 You exceeded your current quota, please check your plan and billing details.Node.js
References: jsonwebtoken README: errors and options (clockTolerance), RFC 7519: the exp claim
Spotted a mistake? Email hello@greatwork.company. About Runheed.