Authon Blog
debugging3 min read

Debugging "JWT Token Expired" Errors: A Complete Guide

Why your JWT tokens expire unexpectedly and how to fix it. Covers clock skew, refresh token rotation, and common pitfalls with token-based auth.

AW
Alan West
Authon Team
Debugging "JWT Token Expired" Errors: A Complete Guide

The Problem

You've shipped your auth integration. It works perfectly in development. Then production users start reporting random logouts and "token expired" errors. Sound familiar?

JWT expiration issues are one of the most common authentication bugs. Let's break down why they happen and how to fix them.

Understanding JWT Expiration

A JWT contains an exp claim — a Unix timestamp indicating when the token becomes invalid:

json
{
  "sub": "user_123",
  "email": "user@example.com",
  "exp": 1711036800,
  "iat": 1711033200
}

When your server validates a token, it checks if Date.now() / 1000 > token.exp. If true, the token is rejected.

Common Causes

1. Clock Skew

The most insidious cause. If your server's clock is even slightly ahead of the auth server's clock, tokens can appear expired immediately after issuance.

Fix: Add a clock tolerance (usually 30-60 seconds):
typescript
import jwt from 'jsonwebtoken'

const decoded = jwt.verify(token, secret, {
  clockTolerance: 60 // allow 60 seconds of skew
})

2. Short Token Lifetimes Without Refresh

Setting access tokens to expire in 5 minutes is good security practice. But if you don't implement token refresh, users get logged out constantly.

Fix: Implement silent refresh:
typescript
async function fetchWithAuth(url: string) {
  let token = getAccessToken()
  
  if (isTokenExpiringSoon(token, 60)) {
    token = await refreshAccessToken()
  }
  
  return fetch(url, {
    headers: { Authorization: `Bearer ${token}` }
  })
}

3. Timezone Confusion

Using new Date() to create expiration times without accounting for UTC can cause tokens that expire hours early or late.

Fix: Always use UTC for token timestamps:
typescript
// Wrong
const exp = new Date().getTime() / 1000 + 3600

// Right (same result, but be explicit)
const exp = Math.floor(Date.now() / 1000) + 3600

4. Caching Stale Tokens

If you cache JWTs (e.g., in localStorage or a CDN), users might continue using expired tokens.

Fix: Always check expiration before use and implement proactive refresh.

The Authon Approach

Authon handles token refresh automatically. The SDK intercepts API calls, checks token expiration, and refreshes silently:

typescript
import { createAuthonClient } from '@authon/browser'

const authon = createAuthonClient({
  projectId: 'proj_xxx',
  // Tokens are refreshed automatically
  // No manual refresh logic needed
})

// Just use it — refresh is handled internally
const user = await authon.getUser()

Debugging Checklist

When you encounter JWT expiration issues:

  • Decode the token at jwt.io and check the exp claim
  • Compare the exp timestamp with your server's current time
  • Check if your server has NTP synchronization enabled
  • Verify your refresh token rotation is working
  • Look for race conditions in concurrent refresh attempts
  • Check if tokens are being cached past their expiration
  • Prevention

    • Use a well-maintained auth library instead of rolling your own JWT logic
    • Set reasonable token lifetimes (15 min access, 7 day refresh)
    • Implement token refresh with retry logic
    • Monitor token validation failures in production
    • Use NTP on all servers

    Conclusion

    JWT expiration errors are almost always a symptom of missing refresh logic or clock synchronization issues. Using a managed auth provider like Authon eliminates most of these problems by handling token lifecycle automatically.

    Debugging "JWT Token Expired" Errors: A Complete Guide | Authon Blog