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:
{
"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):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: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.
// Wrong
const exp = new Date().getTime() / 1000 + 3600
// Right (same result, but be explicit)
const exp = Math.floor(Date.now() / 1000) + 36004. 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:
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:
exp claimexp timestamp with your server's current timePrevention
- 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.