JWT Expired Errors Explained — Causes, Fixes and How to Debug Them
JWT expired errors are one of the most common authentication issues developers face. This guide explains exactly why they happen, how to debug them instantly and how to prevent them in your application.
Understanding the JWT exp claim
Every JWT token contains an exp claim in its payload. This is a Unix timestamp — the number of seconds since January 1, 1970 — representing the exact moment the token becomes invalid.
When your server receives a JWT it checks the current time against theexpvalue. If the current time is greater thanexpthe token is rejected with a 401 Unauthorized response.
// Decoded JWT payload
{
"sub": "user_123",
"name": "James Whitfield",
"role": "admin",
"iat": 1716000000, // issued at — May 18, 2026 10:00:00
"exp": 1716003600 // expires at — May 18, 2026 11:00:00
}
// 1 hour later the token is expired
// Any request using it returns 401💡 Use our JWT Decoder to instantly see when your token was issued and when it expires — in human readable format.
The 6 most common causes of JWT expired errors
1. Token genuinely expired — short expiry window
The most common cause. Many authentication libraries set very short default expiry times. For example Firebase Auth tokens expire after 1 hour. Supabase access tokens expire after 1 hour. If your user leaves their session open and comes back later their token has expired.
// Common expiry settings Firebase: 1 hour (3600 seconds) Supabase: 1 hour (3600 seconds) NextAuth: 30 days (default, configurable) Custom: varies — check your jwt.sign() options
2. Clock skew between client and server
Clock skew happens when the time on your server is slightly different from the time on the client or the token issuer. Even a difference of a few minutes can cause tokens to appear expired before they actually are.
// Example of clock skew problem
Token issued at: 10:00:00 (issuer server time)
Token expires at: 11:00:00
Your server time: 11:00:03 (3 seconds ahead)
Result: Token appears expired! ❌
// Fix — add clockTolerance to your JWT verification
jwt.verify(token, secret, { clockTolerance: 60 })3. Token not being refreshed
Most authentication systems use two tokens — an access token with a short expiry and a refresh token with a long expiry. If your application is not automatically refreshing the access token using the refresh token the user will get expired errors after the access token lifetime.
// Correct flow
Access token expires (1 hour)
↓
Use refresh token to get new access token
↓
Continue session seamlessly
// Wrong flow
Access token expires (1 hour)
↓
User gets 401 error ❌
↓
User forced to log in again4. Wrong timezone on server
JWT timestamps are always in UTC. If your server is configured with the wrong timezone it may calculate expiry times incorrectly. This is especially common in containerized deployments where the container timezone differs from the host.
# Check your server timezone date # Should output UTC time # If it shows local time your JWT validation # may have timezone related issues # Fix for Docker containers ENV TZ=UTC
5. Token stored in localStorage for too long
If you store JWT tokens in localStorage they persist across browser sessions. A user who logged in last week may still have the old token in localStorage. When they visit your site the stored token is sent but it expired days ago.
6. Server time drifted after deployment
After long running deployments server time can drift. NTP (Network Time Protocol) synchronization issues can cause the server clock to be minutes ahead or behind. Tokens issued before the drift may appear expired.
How to debug JWT expired errors step by step
Decode your token
Copy your JWT token and paste it into ourJWT Decoderto instantly see the expiry time in human readable format.
Check the exp claim manually
// In browser console
const token = "your.jwt.token"
const payload = JSON.parse(atob(token.split('.')[1]))
const expiry = new Date(payload.exp * 1000)
console.log("Expires:", expiry.toLocaleString())
console.log("Expired:", expiry < new Date())Check for clock skew
// Compare server time vs token exp
const now = Math.floor(Date.now() / 1000)
const exp = payload.exp
const diff = exp - now
console.log("Seconds until expiry:", diff)
// Negative = already expired
// Small positive = about to expireCheck your refresh token logic
Verify your application is intercepting 401 responses and automatically refreshing the access token before retrying the request.
Permanent fixes
Fix 1 — Implement token refresh interceptor
// Axios interceptor example
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
const newToken = await refreshAccessToken()
error.config.headers['Authorization'] =
'Bearer ' + newToken
return axios(error.config)
}
return Promise.reject(error)
}
)Fix 2 — Add clock tolerance to verification
// Node.js / jsonwebtoken
jwt.verify(token, secret, {
clockTolerance: 60 // 60 seconds tolerance
})
// Java / jjwt
Jwts.parserBuilder()
.setAllowedClockSkewSeconds(60)
.build()
.parseClaimsJws(token)Fix 3 — Proactive token refresh
// Refresh token 5 minutes before expiry
function scheduleRefresh(token) {
const payload = JSON.parse(atob(token.split('.')[1]))
const expiresIn = payload.exp - Date.now() / 1000
const refreshIn = (expiresIn - 300) * 1000 // 5 min early
setTimeout(async () => {
await refreshAccessToken()
}, refreshIn)
}Debugging Supabase JWT expired errors
Supabase access tokens expire after 1 hour by default. The Supabase client library handles refresh automatically but there are cases where it fails.
// Common Supabase JWT issue
// If you store the session manually and restore it
// the refresh token may have expired too
// Correct way — let Supabase handle sessions
const { data: { session } } = await supabase.auth.getSession()
// Check if session is valid before using it
if (!session) {
// Redirect to login
router.push('/login')
return
}
// Supabase auto-refresh is enabled by default
// Make sure you haven't disabled it
const supabase = createClient(url, key, {
auth: {
autoRefreshToken: true, // ← must be true
persistSession: true,
}
})Debugging NextAuth JWT expired errors
// NextAuth — check session expiry
import { useSession } from 'next-auth/react'
const { data: session, status } = useSession()
// Session status values:
// "loading" — session is being fetched
// "authenticated" — user is logged in
// "unauthenticated" — no valid session
// Extend session in NextAuth config
// auth.config.ts
export const authConfig = {
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async jwt({ token, user }) {
if (user) token.id = user.id
return token
}
}
}Debug your JWT token instantly
Paste any JWT token and see the expiry time, claims and signature details. No data leaves your browser.
Open JWT Decoder →