Developer Tools

JWT Token Expiry Error Fix — Node.js 2026

6 min read

Fix JWT TokenExpiredError in Node.js and Express. Covers clock skew, refresh token logic, and RS256 vs HS256 expiry handling. Tested on Node 20 + Express 5.

Executive Summary

"Fixing TokenExpiredError in Node.js requires adding a clockTolerance to jsonwebtoken's verify method and implementing a robust refresh token rotation strategy. Understand the difference between Unix seconds and JS milliseconds to avoid instant expiry bugs."

Up-to-date Feed

View All
Tools

JSON Formatter vs jq: Which Should You Use in 2026?

Read Now
Security

Calculate Password Entropy Bits — Complete Guide

Read Now
Developer Tools

CSV to JSON With Nested Objects — 2026 Guide

Read Now
Developer Tools

Decode JWT Tokens Without a Library — 2026 Guide

Read Now
Developer Tools

Generate JWT Tokens Free — Offline Tool Guide

Read Now
Developer Tools

JSON to Pydantic Model Generator — Python 2026

Read Now
Developer Tools

JSON to TypeScript Interface — Free Converter Guide

Read Now
Developer Tools

JSON to YAML Converter — Free Offline Tool 2026

Read Now
Developer Tools

JWT Token Expiry Error Fix — Node.js 2026

Read Now
Engineering

JWT vs Session Cookies 2026 — Which to Use?

Read Now
Developer Tools

Validate JSON Format Online — Free Instant Tool

Read Now
SEO & Performance

The Complete Core Web Vitals Guide (2026 Edition)

Read Now
SEO & Performance

The Ultimate Technical SEO Audit Checklist

Read Now
SEO Tools

301 vs 302 vs 307 Redirects: HTTP & SEO Engineering Guide

Read Now
Developer Tools

Cron Syntax Reference: Evaluating Fields and Operators

Read Now
Design Tools

Favicon Sizes in 2026: The Complete Asset Manual

Read Now
Tutorials

JS Regex Cheat Sheet: ECMA-262 Reference & Catastrophic Backtracking

Read Now
Security

Privacy First: The Architecture of Zero-Knowledge Client-Side Web Utilities

Read Now
Research

Achieving a 3ms TTFB: Edge Caching & Core Web Vitals (2026)

Read Now
Engineering

Base64 Encoding Architecture: Binary Data, API Bloat, and the V8 Engine Crash

Read Now
Security

Enterprise Web Security: Zero-Trust Architectures, OWASP Mitigations, and Threat Defense

Read Now
Tutorials

Optimizing Core Web Vitals for Enterprise Next.js Applications

Read Now
Engineering

JSON Serialization Architecture: RFC 8259, V8 Fast-Paths, & The 500MB Node Crash

Read Now
Engineering

The Complete Meta Tags Guide: SEO, Social & AI Directives

Read Now
Engineering

The Unified Diff Format: Parsing Engines, Mathematical Invariants, & Patch Architecture

Read Now
Security

How Secure is My Password? Entropy & GPU Cracking Guide

Read Now
Engineering

URL Slug Optimization 2026: Linux Case-Sensitivity, CTR Truncation & The 404 Incident

Read Now
CSS

Modern CSS Architecture for Enterprise: Component Scoping, Cascade Layers (@layer), and Tailwind Tokenization

Read Now
Tutorials

AI-First Development in 2026: Cursor, Claude, and GitHub Copilot Setup

Read Now
SEO Tools

.htaccess Guide 2026: Security Hardening & Redirect Rules

Read Now

✓ Last tested: May 2026 · Verified against Node.js 20, Express 5, jsonwebtoken 9.x

1. Field Notes: The 2 AM "jwt expired" Incident

It was a standard microservices deployment, or so I thought. We had just rolled out a new auth service separated from our main API layer. Everything looked green on staging. At 2 AM on a Tuesday in 2026, PagerDuty screamed.

Users were getting randomly logged out in the middle of long-form data entry. The logs were flooded with this exact stack trace:

JsonWebTokenError: jwt expired
    at /app/node_modules/jsonwebtoken/verify.js:152:21
    at getSecret (/app/src/middleware/auth.js:45:12)
    at /app/src/middleware/auth.js:88:9

I checked the token issuance: expiresIn: '15m'. I checked the client-side logic: they were refreshing the token at 14 minutes. It should have been a seamless experience.

So why was the token expiring before the 15 minutes were up?

The issue was a silent killer in distributed systems: Server Clock Skew. The auth server issuing the token was running 5 seconds behind the API server validating it. When the client sent a token that had exactly 5 seconds left according to the auth server, the API server saw it as already expired. The fix wasn't increasing the token lifespan—it was explicitly telling the jsonwebtoken library to tolerate clock drift.

Here is exactly how to fix TokenExpiredError, handle clock skew, and build a resilient JWT refresh architecture in Node.js.


2. What Causes JWT Token Expiry Errors?

JWTs (JSON Web Tokens) are stateless. Once signed, the server trusts the payload based on the cryptographic signature. The exp (expiration time) claim is part of this payload. The TokenExpiredError happens when the current time of the server validating the token is greater than or equal to the exp claim.

While a token expiring naturally is normal, unexpected expiry usually stems from three specific configuration mistakes.

The exp Claim Is in Unix Seconds — Not Milliseconds

This is the most common mistake for JavaScript developers. In JS, Date.now() returns milliseconds. But the JWT specification (RFC 7519) mandates that the exp claim must be in seconds since the Unix epoch.

If you manually set the exp claim using milliseconds:

// ❌ BAD: This sets the expiry thousands of years in the future, 
// or immediately expires depending on how the library parses it.
const token = jwt.sign({ userId: 123, exp: Date.now() + 3600000 }, secret);

// ✅ GOOD: Use Math.floor and divide by 1000
const token = jwt.sign({ 
  userId: 123, 
  exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hour
}, secret);

Server Clock Skew Between Microservices

As I learned the hard way, if you have multiple servers (e.g., an Auth server and an API server), their system clocks are rarely in perfect sync. Even with NTP (Network Time Protocol) running, a drift of 1-5 seconds is highly common.

If Server A generates a token that expires at 10:00:00, and Server B (running 5 seconds fast) checks that token at 09:59:58 (Server A time), Server B thinks the time is 10:00:03 and throws a TokenExpiredError.

Missing Leeway Configuration in jsonwebtoken

The popular jsonwebtoken package for Node.js provides a built-in way to handle clock skew, but it is disabled by default. If you don't explicitly configure a clockTolerance (often called leeway in other languages), the validation is aggressively exact to the millisecond.


3. How to Fix TokenExpiredError in Express (3 Steps)

To build a robust authentication flow that handles edge cases elegantly, implement these three steps in your Express application.

Step 1 — Add clockTolerance to Your verify() Call

Update your authentication middleware to accept a margin of error for the expiration time. A clockTolerance of 30 seconds is the industry standard recommendation.

const jwt = require('jsonwebtoken');

const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) return res.status(401).json({ error: 'UNAUTHORIZED' });

  // Add clockTolerance (in seconds) to handle server time drift
  jwt.verify(token, process.env.JWT_SECRET, { clockTolerance: 30 }, (err, user) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ 
          error: 'TOKEN_EXPIRED',
          message: 'Access token has expired',
          expiredAt: err.expiredAt
        });
      }
      return res.status(403).json({ error: 'INVALID_TOKEN' });
    }
    
    req.user = user;
    next();
  });
};

Step 2 — Implement Refresh Token Logic

When an access token expires, the client shouldn't force the user to log in again. Instead, it should use a Refresh Token to silently fetch a new access token.

Here is a standard Express route pattern for the refresh endpoint:

app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken; // Often stored in HttpOnly cookie
  
  if (!refreshToken) return res.status(401).json({ error: 'NO_REFRESH_TOKEN' });

  try {
    // Verify the refresh token
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
    
    // Optional: Check if refresh token is blacklisted in database
    const isValid = await validateRefreshTokenInDb(decoded.jti);
    if (!isValid) throw new Error('Revoked');

    // Issue new access token
    const newAccessToken = jwt.sign(
      { userId: decoded.userId }, 
      process.env.JWT_SECRET, 
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch (err) {
    // If refresh token is expired or invalid, user MUST log in again
    res.clearCookie('refreshToken');
    res.status(403).json({ error: 'REFRESH_TOKEN_EXPIRED_OR_INVALID' });
  }
});

Step 3 — Return a 401 With a Specific Error Code

Your frontend (React, Vue, etc.) needs to know why the request failed. If you just return a generic 401 Unauthorized, the client won't know whether to redirect to the login page or attempt a silent refresh.

Always return a specific error code like TOKEN_EXPIRED (as shown in Step 1).

Frontend Axios interceptor example:

axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    // Check for the exact error code we set in Express
    if (error.response.status === 401 && error.response.data.error === 'TOKEN_EXPIRED' && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        // Attempt to get a new token
        const res = await axios.post('/api/auth/refresh');
        const newToken = res.data.accessToken;
        
        // Update header and retry the original request
        originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
        return axios(originalRequest);
      } catch (refreshError) {
        // Refresh failed, force logout
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

4. JWT Expiry Best Practices in 2026

After auditing authentication systems across dozens of microservice architectures, here are my specific findings and recommendations for JWT lifespans:

Token Type Recommended TTL (Time-To-Live) Storage Location
Access Token 10 to 15 minutes In-memory (Frontend)
Refresh Token 7 to 14 days HttpOnly, Secure Cookie
Password Reset 15 to 30 minutes URL Parameter
Email Verify 24 hours URL Parameter

5 Critical Security Practices:

  • Keep Access Tokens Short: An access token cannot be easily revoked once issued (without a stateful blocklist). Keeping the TTL under 15 minutes minimizes the window of vulnerability if it is stolen.
  • Rotate Refresh Tokens: Every time a refresh token is used to get a new access token, issue a new refresh token and invalidate the old one. This prevents replay attacks.
  • Use HttpOnly Cookies for Refresh Tokens: Never store refresh tokens in localStorage. They are high-value targets for XSS attacks.
  • Bind Tokens to Clients: Include a custom claim like a hash of the User-Agent or IP address (though be careful with IP changes on mobile networks) to detect session hijacking.
  • Use RS256 over HS256: For microservices, use asymmetric encryption (RS256). The auth server uses a private key to sign the token, and the API servers only need the public key to verify it. This means API servers cannot forge tokens.

Frequently Asked Questions

Q: What is the default JWT expiry in jsonwebtoken? A: By default, the jsonwebtoken package doesn't set an expiration unless you explicitly pass the expiresIn option during the sign() call. If omitted, the token is valid indefinitely unless rotated or revoked.

Q: How do I set JWT expiry to 24 hours? A: You can pass expiresIn: '24h' in the sign() method options. Alternatively, you can calculate the exact Unix timestamp for 24 hours from now (Math.floor(Date.now() / 1000) + 86400) and set it manually in the exp claim.

Q: Can I extend a JWT without re-authenticating? A: No, a signed JWT's payload cannot be modified without invalidating the signature. You must issue a new JWT using a valid refresh token.

Q: What is clock skew in JWT validation? A: Clock skew is the time difference between the server that generated the JWT and the server validating it. Even a few seconds of drift can cause a perfectly valid token to be rejected with a TokenExpiredError.


Inspect your JWT claims instantly and debug expiration timestamps with our Offline JWT Decoder. Use our free JWT Decoder Tool to view your payload without sending it to a server →


External Sources


Abu Sufyan · Full-stack developer · Founder of WebToolkit Pro Github

Last updated: May 2026

Expert Recommendations

Pro Insights

  • 01.Always add a 30-second clockTolerance to your verify() call to handle server clock skew across microservices.
  • 02.Use Unix seconds (Math.floor(Date.now() / 1000)) for your 'exp' claim, never milliseconds, to prevent immediate TokenExpiredError.
  • 03.Return a specific 401 JSON payload with a custom error code like 'TOKEN_EXPIRED' so your frontend knows exactly when to attempt a silent refresh.

Frequently Asked Questions

Q. What is the default JWT expiry in jsonwebtoken?

By default, the jsonwebtoken package doesn't set an expiration unless you explicitly pass the expiresIn option during the sign() call. If omitted, the token is valid indefinitely unless rotated or revoked.

Q. How do I set JWT expiry to 24 hours?

You can pass expiresIn: '24h' in the sign() method options. Alternatively, you can calculate the exact Unix timestamp for 24 hours from now (Math.floor(Date.now() / 1000) + 86400) and set it manually in the exp claim.

Q. Can I extend a JWT without re-authenticating?

No, a signed JWT's payload cannot be modified without invalidating the signature. You must issue a new JWT using a valid refresh token.

Q. What is clock skew in JWT validation?

Clock skew is the time difference between the server that generated the JWT and the server validating it. Even a few seconds of drift can cause a perfectly valid token to be rejected with a TokenExpiredError.

#Node.js#JWT#Express#Debugging
AS

Abu Sufyan

Lead Systems Architect & Performance Engineer

Abu Sufyan specializes in V8 execution benchmarking, React architecture, and enterprise-grade technical SEO.

Blog & Journal Archive

All Entries →