taptools/blog/JWT Expired Errors
JWTMay 15, 2026 · 8 min read

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 again

4. 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

1

Decode your token

Copy your JWT token and paste it into ourJWT Decoderto instantly see the expiry time in human readable format.

2

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())
3

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 expire
4

Check 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 →