JWT Debugging

JWT Token Expired:
How to Fix It

Getting a 401 Unauthorized? Your JWT has probably expired. Here's how to diagnose it, fix it, and prevent it happening again.

9 min read·Updated Feb 2026

A 401 Unauthorized error is one of the most frustrating things to debug in a JWT-authenticated API. The request looks right. The endpoint hasn't changed. But suddenly, everything returns 401. Nine times out of ten, the token has expired.

This guide walks through exactly how to confirm token expiry is the cause, how to fix it in your frontend and backend, and how to build a refresh token flow that prevents the problem from surfacing to users.

Step 1: Confirm the Token is Actually Expired

Don't assume — decode the token and check the exp claim directly. The exp field is a Unix timestamp (seconds since epoch). If it's less than the current time, the token is expired.

The fastest way without writing code: paste the token into ResourceCentral's JWT Decoder. It decodes entirely in your browser, shows the exp as a human-readable timestamp, and flags it red if expired. Your token never leaves your machine.

Or do it in the browser console:

// Decode JWT payload in browser console
const token = 'eyJhbGci...'; // your token
const payload = JSON.parse(atob(token.split('.')[1]));
const expired = payload.exp < Math.floor(Date.now() / 1000);
console.log('Expires:', new Date(payload.exp * 1000).toLocaleString());
console.log('Expired:', expired);

Or in Python:

import base64, json, time

token = "eyJhbGci..."
payload_b64 = token.split('.')[1]
# Pad base64 if needed
payload_b64 += '=' * (4 - len(payload_b64) % 4)
payload = json.loads(base64.b64decode(payload_b64))

print("Expires:", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(payload['exp'])))
print("Expired:", payload['exp'] < time.time())

Common Causes of Premature Expiry

Clock skew between client and server
The most common culprit. If your server's clock is a few minutes ahead of the client, tokens issued by the server will appear expired immediately on the client. Fix: ensure both use NTP sync. Add a small leeway (60 seconds) in your JWT validation library.
Token lifetime too short
If access tokens are set to expire in 5 minutes, users will hit 401s during normal workflows. 15 minutes is a common minimum; 1 hour works for most applications.
Token not being refreshed
If you issue tokens but have no refresh flow, tokens will eventually expire and users must re-login. Implement a refresh token flow (see below).
Token stored incorrectly
If the token is stored in localStorage and the user opens a new tab, the token may have been issued hours ago. Check the iat (issued at) claim alongside exp.
Wrong token being sent
Particularly common in microservices — the service may be forwarding a token from a different issuer or an older cached token. Log the token's jti (JWT ID) claim to trace it.

The Fix: Implement a Refresh Token Flow

The proper solution to token expiry is a refresh token flow. Instead of forcing users to re-login when an access token expires, the client silently obtains a new access token using a longer-lived refresh token.

Here's the pattern:

1
Issue two tokens on login
An access token (short-lived: 15 min–1 hour) returned in the response body, and a refresh token (long-lived: 7–30 days) set as an HttpOnly cookie.
2
Client detects 401
When any API call returns 401, the client intercepts it before showing an error to the user.
3
Request a new access token
The client calls POST /auth/refresh. The server reads the HttpOnly refresh token cookie, validates it, and issues a new access token.
4
Retry the original request
The client retries the failed request with the new access token. The user sees nothing.
5
Handle refresh token expiry
If the refresh token itself has expired (user hasn't used the app in 30 days), redirect to login. This is the only time a user should be forced to re-authenticate.

Refresh Token Flow: JavaScript Implementation

// Axios interceptor — silently refresh on 401
let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    error ? prom.reject(error) : prom.resolve(token);
  });
  failedQueue = [];
};

axios.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // Queue requests while refresh is in progress
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers['Authorization'] = 'Bearer ' + token;
          return axios(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        // Refresh token is in HttpOnly cookie — no JS access needed
        const { data } = await axios.post('/auth/refresh');
        const newToken = data.access_token;

        axios.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
        processQueue(null, newToken);
        originalRequest.headers['Authorization'] = 'Bearer ' + newToken;
        return axios(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        // Refresh token also expired — redirect to login
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

Choosing the Right Token Lifetime

Application Type Access Token Refresh Token Notes
Banking / Healthcare5–15 min24 hoursShort windows, prompt re-auth on sensitive actions
Standard SaaS15–60 min7–30 daysGood balance of security and UX
Mobile app1 hour90 daysLonger refresh needed for intermittent connectivity
Machine-to-machine API1 hourN/A (Client Credentials)No refresh needed — services request new tokens directly

Debugging Checklist for 401 JWT Errors

Decode Your Token — Free & Private

Check the exp claim, algorithm, and all claims in seconds. Token never leaves your browser.

Open JWT Decoder — Free →

FAQ

Why does my JWT keep expiring? +

Because the exp claim is in the past. Common causes: the token lifetime is too short, there's clock skew between client and server, or there's no refresh token flow to silently renew tokens. Check the decoded exp timestamp first to confirm expiry is the actual issue.

Is it safe to extend the token lifetime to avoid this? +

Not as a long-term fix. Long-lived tokens increase the window of exploitation if stolen. The correct solution is a refresh token flow — keep access tokens short-lived (15–60 min) and use a refresh token to issue new ones automatically.

Can I invalidate a JWT before it expires? +

Not without a server-side blocklist. JWTs are stateless by design — once issued, they're valid until expiry unless your validation checks against a revocation list. If you need immediate revocation (e.g. on logout or account compromise), maintain a blocklist of revoked jti claims, or switch to opaque tokens with a session store.

Where should I store the refresh token? +

In an HttpOnly, Secure, SameSite=Strict cookie. This prevents JavaScript access (protecting against XSS) while still being sent automatically on requests to your refresh endpoint. Never store refresh tokens in localStorage.

Related