Skip to content

JWT Automatic Renewal — Transparent Token Refresh Without User Interruption

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about JWT Automatic Renewal. We cover key concepts, practical examples, and best practices to help you master this topic.

JWT automatic renewal refreshes access tokens in the background before they expire, providing seamless API access without disrupting the user experience.

What You'll Learn

Proactive token refresh strategies, silent renewal on page load, request queuing during refresh, handling refresh failures gracefully, and browser tab synchronization.

Why It Matters

Users abandon applications that interrupt their workflow with login prompts. Automatic renewal makes authentication invisible — the user logs in once and the system handles token lifecycle transparently.

Real-World Use

Google Workspace silently refreshes OAuth tokens. GitHub renews API tokens before expiry. Durga Antivirus Pro renews dashboard session tokens proactively, so security analysts never lose their work mid-investigation.

flowchart LR
    A["App Start"] --> B["Load refresh token"]
    B --> C["Get access token\n(silent refresh)"]
    C --> D["API calls proceed"]
    D --> E{"Token 80% expired?"}
    E -->|"No"| D
    E -->|"Yes"| F["Proactive refresh\nin background"]
    F --> G["New access token"]
    G --> D
    D --> H{"API returns 401?"}
    H -->|"No"| D
    H -->|"Yes"| I["Attempt refresh\n+ retry request"]
    I --> J{"Refresh OK?"}
    J -->|"Yes"| D
    J -->|"No"| K["Redirect to login"]
    style A fill:#dbeafe,stroke:#2563eb
    style D fill:#dcfce7,stroke:#16a34a
    style K fill:#fecaca,stroke:#dc2626

Code Example: Proactive Token Refresh with Timer

class AutoRenewClient {
  constructor(config) {
    this.baseURL = config.baseURL;
    this.refreshEndpoint = config.refreshEndpoint || '/api/auth/refresh';
    this.tokenExpiryBuffer = config.bufferSeconds || 120; // Refresh 2min early
    this.onSessionExpired = config.onSessionExpired || (() => {});
    this.refreshPromise = null;
    this.refreshTimer = null;
    this._setupRefreshTimer();
  }

  setTokens(access, refresh) {
    this.accessToken = access;
    this.refreshToken = refresh;
    this.scheduleProactiveRefresh(access);
  }

  scheduleProactiveRefresh(token) {
    if (this.refreshTimer) clearTimeout(this.refreshTimer);

    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      const expiresAt = payload.exp * 1000;
      const now = Date.now();
      const timeToExpiry = expiresAt - now;
      const refreshAt = Math.max(
        0,
        timeToExpiry - (this.tokenExpiryBuffer * 1000)
      );

      if (timeToExpiry <= 0) {
        this.proactiveRefresh();
        return;
      }

      this.refreshTimer = setTimeout(
        () => this.proactiveRefresh(),
        refreshAt
      );
    } catch {
      // Token decode failed — refresh now
      this.proactiveRefresh();
    }
  }

  async proactiveRefresh() {
    if (this.refreshPromise) return this.refreshPromise;

    this.refreshPromise = (async () => {
      const resp = await fetch(`${this.baseURL}${this.refreshEndpoint}`, {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refresh_token: this.refreshToken })
      });

      if (resp.ok) {
        const data = await resp.json();
        this.accessToken = data.access_token;
        if (data.refresh_token) this.refreshToken = data.refresh_token;
        this.scheduleProactiveRefresh(data.access_token);
      }
    })();

    await this.refreshPromise;
    this.refreshPromise = null;
  }

  async fetch(path, options = {}) {
    const headers = { ...options.headers };
    if (this.accessToken) {
      headers['Authorization'] = `Bearer ${this.accessToken}`;
    }

    const resp = await fetch(`${this.baseURL}${path}`, { ...options, headers });

    if (resp.status === 401 && this.refreshToken) {
      await this.proactiveRefresh();
      headers['Authorization'] = `Bearer ${this.accessToken}`;
      return fetch(`${this.baseURL}${path}`, { ...options, headers });
    }

    return resp;
  }
}

Code Example: Silent Refresh on Page Load

from flask import Flask, request, jsonify, make_response
import jwt, datetime, secrets

app = Flask(__name__)
SECRET = "silent-refresh-secret"

@app.route("/api/auth/silent-refresh", methods=["POST"])
def silent_refresh():
    """Handles silent token refresh during app initialization."""
    refresh_token = request.json.get("refresh_token")
    if not refresh_token:
        return jsonify({"error": "Refresh required"}), 401

    # Validate refresh token (simplified)
    user = validate_and_get_user(refresh_token)
    if not user:
        return jsonify({"error": "Session expired"}), 401

    access_token = jwt.encode({
        "sub": user,
        "iat": datetime.datetime.utcnow(),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15),
        "jti": secrets.token_hex(16)
    }, SECRET, algorithm="HS256")

    return jsonify({
        "access_token": access_token,
        "expires_in": 900,
        "token_type": "Bearer"
    })

Client initialization:

// On app start, attempt silent refresh
async function initializeApp() {
  const store = getRefreshTokenStore();
  const refreshToken = await store.get();

  if (refreshToken) {
    try {
      const resp = await fetch('/api/auth/silent-refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refresh_token: refreshToken })
      });

      if (resp.ok) {
        const data = await resp.json();
        client.setTokens(data.access_token, refreshToken);
        return; // App is authenticated
      }
    } catch {
      // Network error — app works offline, retries later
    }
  }

  // No valid session — show login
  showLoginPage();
}

Code Example: Request Queue During Refresh

class QueuedAutoRenewClient {
  constructor() {
    this.accessToken = null;
    this.refreshToken = null;
    this.isRefreshing = false;
    this.queue = [];
  }

  async fetch(path, options = {}) {
    if (this.isRefreshing) {
      // Queue this request — it will be retried after refresh
      return new Promise((resolve, reject) => {
        this.queue.push({ resolve, reject, path, options });
      });
    }

    return this._executeWithRetry(path, options);
  }

  async _executeWithRetry(path, options, retried = false) {
    const headers = { ...options.headers };
    if (this.accessToken) {
      headers['Authorization'] = `Bearer ${this.accessToken}`;
    }

    const resp = await fetch(path, { ...options, headers });

    if (resp.status === 401 && !retried) {
      await this._refreshAndRetry();
      return this._executeWithRetry(path, options, true);
    }

    return resp;
  }

  async _refreshAndRetry() {
    this.isRefreshing = true;

    try {
      const resp = await fetch('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include'
      });

      if (resp.ok) {
        const data = await resp.json();
        this.accessToken = data.access_token;
      }
    } finally {
      this.isRefreshing = false;
    }

    // Drain queue — all queued requests now have a valid token
    const queued = this.queue.splice(0);
    for (const item of queued) {
      item.resolve(this._executeWithRetry(item.path, item.options, true));
    }
  }
}

Common Mistakes

1. Proactive Refresh on Every Request

Refreshing the token before every API call defeats the purpose of token expiry and creates unnecessary load. Only refresh when the token is near expiry.

2. Not Handling Concurrent Tab Refresh

Multiple browser tabs may all try to refresh simultaneously. Use a shared worker or broadcast channel to coordinate token refresh across tabs.

3. Silent Refresh Without Error Feedback

If the refresh fails silently and the user makes a mutating request, the data could be lost. Show a brief "Session expired, reconnecting" indicator.

4. Refresh Race Condition

Two API calls that both get 401 before the first refresh completes will both trigger refresh. Use a promise-based lock to ensure only one refresh runs.

5. Ignoring Clock Drift

Server and client clocks may differ. Always use the server-issued exp claim rather than client-side calculations for expiry decisions.

Practice Questions

  1. Why use proactive refresh instead of waiting for 401?
  2. How does a request queue prevent data loss during refresh?
  3. What is the typical buffer time for proactive refresh?
  4. How do you coordinate refresh across multiple browser tabs?
  5. Why should you use the exp claim instead of client timers?

Answers:

  1. Proactive refresh prevents visible 401 errors and request failures. The token is renewed before it expires, so all requests succeed on the first attempt.
  2. When a refresh is in progress, new requests are queued. After refresh completes, all queued requests are retried with the new token.
  3. 60-180 seconds before the token's actual expiry. This gives enough time for the refresh to complete before the token expires.
  4. Use BroadcastChannel API to notify other tabs when a refresh occurs, or use a SharedWorker that manages the token centrally.
  5. Server clocks are authoritative. Client timers may drift due to system sleep, time zone changes, or clock adjustments. Always decode the JWT and read exp.

Challenge: Build a frontend auth client that implements proactive token refresh with a configurable buffer, request queuing during refresh, and cross-tab coordination.

FAQ

Does automatic renewal reduce security?

No. The access token still expires after a short duration (e.g., 15 minutes). Automatic renewal is a UX convenience, not a security compromise.

What if the proactive refresh fails?

The client should keep the old token and try again later. If the old token expires before the next refresh attempt, fall back to the synchronous 401+refresh pattern.

Can I use service workers for automatic renewal?

Yes. A service worker can intercept fetch requests, check token expiry, and refresh proactively without the application code knowing about it.

How do I test automatic renewal?

Mock the token expiry date to be very short. Verify that proactive refresh fires before expiry and that requests still succeed after the original token expires.

Does this work with server-side rendering?

Yes, but SSR cannot refresh tokens since there is no user session. Pass the access token from the server-side render and let the client-side code handle renewal.

Mini Project

Build a React application with an automatic token renewal service that proactively refreshes tokens before expiry, queues requests during refresh, and gracefully handles refresh failures with a re-login prompt.

What's Next

Now learn about JWT Blocklist with Redis for centrally managing revoked tokens across distributed services.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro