JWT — token expired error even with a fresh login
Diagnose JWT expiry errors by checking the exp claim, clock skew and token refresh timing.
Symptoms
- TokenExpiredError: jwt expired
- The user is logged out almost immediately
- It fails on the server but not locally
- Refresh logic does not run before expiry
Possible causes
- The token TTL is very short and clocks differ between issuer and verifier
- Server time is ahead of the token issuer time
- Refresh is attempted only after a request already failed
- The exp claim is interpreted in the wrong unit
Diagnosis steps
Decode the claims without verifying
node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64'))" <token>Interpretation
Reading iat and exp shows the intended lifetime and whether it is already past.
Compare clocks
date -uInterpretation
A verifier clock ahead of the issuer makes a valid token appear expired, which is common in distributed systems.
Allow a small leeway
jwt.verify(token, secret, { clockTolerance: 5 })Interpretation
A few seconds of tolerance absorbs minor skew without weakening security.
Root cause
The token was considered expired by the verifier because its exp claim was in the past, either genuinely due to a short TTL, or apparently because the verifier clock ran ahead of the issuer clock.
Fix
Set a TTL that matches the use case, synchronize clocks across services, allow a small clock tolerance, and refresh tokens proactively before they expire rather than after a failed request.
Verify the fix
Log in and use the token through its expected lifetime and confirm it is accepted, and confirm refresh happens before expiry under normal use.
Symptom walkthrough
A request fails with TokenExpiredError even though the user just logged in. Decoding the token shows its exp claim, and the question is whether the token genuinely expired or the verifier thinks so because its clock is ahead of the issuer. Very short TTLs combined with clock skew between services make valid tokens appear expired, which is confusing and easy to misattribute to a login bug.
Fix in practice
Choose a TTL that fits the session, keep clocks synchronized across services, and permit a small clock tolerance to absorb minor skew. Refresh tokens before they expire based on their remaining lifetime rather than reacting to a failed request, which avoids a window of errors.
Verification
Log in and exercise the token across its expected lifetime and confirm it is accepted, then confirm refresh occurs before expiry during normal traffic. Acceptance through the lifetime plus proactive refresh confirms the fix.
Diagnostic commands
Check the behavior with commands like these:
node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64'))" <token>
date -u
jwt.verify(token, secret, { clockTolerance: 5 })