JWT Token Expired: 9 Quick Fixes for Real 401 Errors
A JWT token expired error means the server compared the exp claim with its own clock and found the token too old. Most of the time that is the whole story. When the token looks fresh and the error persists, the cause sits in one of eight other places, and this guide shows you how to find it in about a minute.
Step one: read the claims of the token behind the JWT token expired error
A JSON Web Token has three parts separated by dots: header, payload and signature. The first two are base64url encoded JSON, not encrypted, so anyone holding the token can read them. You need no key for that. Here is a real example I generated, with a lifetime of 15 minutes.
Sample token behind a JWT token expired error
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyXzQyIiwiYXVkIjoiYXBpLmV4YW1wbGUuY29tIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjY1MDAsInNjb3BlIjoicmVhZDpvcmRlcnMifQ.SOb9ntWowAoJACZ6_lINwtUOP2AnEFTsB-i3n227PxADecoded payload for the JWT token expired check
{
"iss": "https://auth.example.com",
"sub": "user_42",
"aud": "api.example.com",
"iat": 1767225600,
"exp": 1767226500,
"scope": "read:orders"
}| Claim | Value | What it tells you |
|---|---|---|
| iat | 1767225600 | Issued at 1 January 2026, 00:00:00 UTC |
| exp | 1767226500 | Expires 900 seconds later, at 00:15:00 UTC |
| aud | api.example.com | Only this API should accept the token |
| iss | https://auth.example.com | Only this issuer should be trusted |
| sub | user_42 | The user the token is about |
The JWT Decoder does this split for you, labels each standard claim and shows a valid or expired badge computed from your local clock. Paste the token with or without the Bearer prefix.
Nine causes behind an expired or invalid token
Error wording for a JWT token expired failure differs between libraries. Node packages raise names such as TokenExpiredError, and PyJWT raises ExpiredSignatureError. The table maps what you see to the usual cause and the fix.
| Symptom | Likely cause | Fix |
|---|---|---|
| Expired error, exp is in the past | The token is simply old | Request a new access token with the refresh token or sign in again |
| Expired error, exp is in the future | Clock skew between servers | Sync clocks with NTP and allow a small leeway |
| Not active yet, right after login | nbf or iat is ahead of the validator clock | Fix clock skew or issue nbf slightly in the past |
| Token never expires | exp written in milliseconds | Use whole seconds |
| Malformed or wrong number of segments | Quotes, spaces, line breaks or a Bearer prefix in the value | Strip the extras so only three parts remain |
| Invalid signature | Wrong secret or key, wrong algorithm, key rotation | Verify with the issuer key in a server library, check the kid header |
| Invalid audience | aud differs from what the API expects | Align aud with the API configuration |
| Invalid issuer | iss differs from the configured issuer | Match the exact issuer string, including the trailing slash |
| Works in a decoder, rejected by the API | Decoding does not verify or check revocation | Verify the signature and test the revocation list on the server |
Paste the token behind your JWT token expired error, read exp, nbf and aud at a glance, and see whether it has expired by your own clock. The token never leaves your browser.
Open the JWT DecoderClock skew: rejected on a fresh token
Two machines rarely agree to the second. If your validator runs 40 seconds ahead of the issuer, it declares a token dead 40 seconds early. Take the sample token with exp 1767226500.
| Item | Value |
|---|---|
| Issued at | 00:00:00 UTC |
| exp | 00:15:00 UTC |
| Rejected from, real time | 00:14:20 UTC |
| Usable lifetime | 860 seconds |
| Lifetime lost | 40 seconds, 4.4 percent |
On a 15 minute token that loss is a nuisance. On a token that lasts 60 seconds it can mean the token is dead on arrival. Most libraries offer a leeway or clock tolerance option, often set to something like 30 or 60 seconds. Use a small value and fix the clocks as well. A large leeway weakens expiry.
The milliseconds mistake
JavaScript time is in milliseconds, JWT time is in seconds. Developers who write Date.now() straight into exp create tokens that appear to last for millennia. Count the digits.
| Digits | Unit | Example | Reads as |
|---|---|---|---|
| 10 | Seconds, correct for JWT | 1767226500 | 1 January 2026, 00:15:00 UTC |
| 13 | Milliseconds, wrong for JWT | 1767226500000 | Year 57971 when read as seconds |
The reverse problem exists too. If a client multiplies exp by 1000 for a JavaScript date, a token with a 13 digit value ends up unreadable. Paste values into the Unix Timestamp Converter when you need to see the date behind a number.
Reading a payload proves nothing about who wrote it. Anyone can craft a token with any claims. The signature is what binds the claims to the issuer, and only a server holding the right key can check it. Never paste a secret key into a web page.
A diagnostic order that saves time
- Confirm the structureThe value must have three parts, or two for an unsigned token. If it does not, the problem is in how your client stores or sends it.
- Read exp, nbf and iatConvert each to a date. Check the digit count first, so you spot milliseconds.
- Compare with the validator clock that said JWT token expiredRun the date command on the server that rejected the token. A difference of more than a few seconds explains many intermittent failures.
- Check aud and issCompare them character by character with the API settings. A trailing slash is enough to fail.
- Check alg and kid in the headerThe algorithm must match what the server accepts. A kid that points to a rotated key explains a signature failure that started suddenly.
- Verify on the serverUse your server side library with the right key. Only that step proves the token is genuine and not revoked.
Stop the error from reaching users
Fixing the cause is half the job. The other half is making sure a JWT token expired response does not become a visible failure. A short lived access token is supposed to expire, so the client should expect it and recover quietly.
Fragile client on an expired response
- Sends the same token until the server says no
- Shows an error page on the first 401
- Stores expiry as local time or in milliseconds
- Retries the failed request without a new token
Resilient client
- Reads exp and refreshes shortly before it passes
- Catches one 401, refreshes once, then repeats the request
- Stores expiry as seconds since 1970 UTC
- Signs the user out only if the refresh itself fails
Refresh a little early to avoid a JWT token expired error
function needsRefresh(token, marginSeconds) {
var payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
var now = Math.floor(Date.now() / 1000);
return payload.exp - now < marginSeconds;
}
// refresh when fewer than 60 seconds remain
if (needsRefresh(accessToken, 60)) { accessToken = await refresh(); }A margin of 60 seconds protects against small clock differences and slow networks. It also means the user never sees the moment of expiry. Keep the refresh call itself behind a lock, so ten parallel requests trigger one refresh and not ten.
| Token | Typical lifetime | Where it goes | If it expires |
|---|---|---|---|
| Access token | Minutes | Sent with each API call | Refresh silently |
| Refresh token | Days or weeks | Sent only to the token endpoint | Ask the user to sign in |
| ID token | Minutes | Read by the client once after login | Not used for API calls |
Lifetimes are policy, not law. Your identity provider sets them, and the right values depend on how much damage a stolen token could do. The point is the pattern: short for access, longer for refresh, and a clear path from one to the other.
Related tools for the debugging session
If you want to see how a segment becomes text, the Base64 Encoder and Decoder shows the standard alphabet, while JWT swaps plus and slash for hyphen and underscore. To understand how a keyed signature is built, try the HMAC Generator.
The token should be good until 00:15:00 UTC. A rejection 30 seconds early points to clock skew on the validating machine.
Questions people ask about a JWT token expired error
How do I check if a JWT token expired?
Decode the payload, read the exp claim and compare it with the current Unix time in seconds. If exp is smaller than now, the token is expired.
Can I decode a JWT without the secret key?
Yes. The header and payload are base64url encoded, not encrypted. Only verifying the signature needs the key.
Why is my JWT invalid even though the JWT token expired check passes?
Check the signature key, algorithm, aud and iss values, nbf and clock skew. Any one of them can make a valid looking token fail.
What is the difference between exp, nbf and iat?
Exp is the time after which the token must be rejected, nbf is the time before which it must not be accepted, and iat is when it was issued. All three are numbers of seconds since 1970 UTC.
Should I extend the lifetime to stop the JWT token expired errors?
No. Short lived access tokens limit damage if one leaks. Use a refresh token to get new ones, and fix the underlying clock or unit problem.
Is it safe to paste a JWT into an online decoder?
Treat a live token like a password. A decoder that runs in your browser and makes no network request limits the risk, but prefer test tokens for anything sensitive.
Sources: RFC 7519, JSON Web Token (JWT)
