JWT Automatic Renewal — Transparent Token Refresh Without User Interruption
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
- Why use proactive refresh instead of waiting for 401?
- How does a request queue prevent data loss during refresh?
- What is the typical buffer time for proactive refresh?
- How do you coordinate refresh across multiple browser tabs?
- Why should you use the
expclaim instead of client timers?
Answers:
- Proactive refresh prevents visible 401 errors and request failures. The token is renewed before it expires, so all requests succeed on the first attempt.
- When a refresh is in progress, new requests are queued. After refresh completes, all queued requests are retried with the new token.
- 60-180 seconds before the token's actual expiry. This gives enough time for the refresh to complete before the token expires.
- Use BroadcastChannel API to notify other tabs when a refresh occurs, or use a SharedWorker that manages the token centrally.
- 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
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