Refresh tokens are the backbone of modern authentication flows. When they work correctly, users stay logged in for days or weeks without thinking about it. When they break, users get kicked out every few hours and your support inbox fills up fast.
If you are dealing with a refresh token that keeps expiring too soon, you are not alone. This is one of the most common OAuth 2.0 problems reported across developer communities on Reddit, GitHub, and Stack Overflow. The good news is that the root cause is usually one of a small number of fixable issues.
In this guide, we cover every major cause of premature refresh token expiration and walk through step-by-step fixes. Whether you are using Auth0, Microsoft identity platform, Google OAuth, or a custom OAuth server, you will find actionable solutions here.
Our team has spent years building authentication systems and debugging token issues in production. We have seen every flavor of this problem, from tokens dying after 12 hours to race conditions invalidating tokens under load. The fixes below come from real production experience and community-verified solutions.
Table of Contents
Quick Answer: The Most Common Fix for Refresh Token Expiry
What to do when a refresh token expires? When a refresh token expires, you must redirect the user to re-authenticate through the full OAuth authorization flow to obtain a new token pair. To prevent this, ensure your backend stores the new refresh token returned on each refresh cycle, configure appropriate idle and maximum lifetime values, and enable refresh token rotation with automatic reuse detection.
The single most common cause of premature refresh token expiry is failing to update the stored token in your database after each refresh. When you exchange a refresh token for a new access token, many providers return a new refresh token alongside it. If your backend keeps using the old one, it will eventually be rejected, and the user will need to log in again.
We will walk through this fix and every other common cause in detail below.
Why Refresh Tokens Expire Prematurely
Before jumping into fixes, it helps to understand why your refresh token expires too soon. There are seven common root causes. Identifying which one applies to your situation will save you hours of debugging.
1. Backend Not Updating the Stored Refresh Token
This is the number one cause reported across developer forums. Many OAuth providers, including Google, return a brand-new refresh token each time you exchange the current one. Your backend must capture this new token and replace the old one in your database.
If your code only stores the access token from the refresh response and discards the new refresh token, the old token becomes stale. Some providers invalidate the old token immediately after issuing a new one. This makes it look like your refresh token expires after a single use.
As one Reddit developer put it after solving their 12-hour expiry issue: “Google returns a new refresh token every time you use the current one. You must store the new one or it will appear to expire.” This single oversight accounts for a huge percentage of premature expiry reports.
2. Idle Timeout Configuration
Most OAuth providers support an idle refresh token lifetime. This setting controls how long a refresh token can remain unused before it expires. If a user does not interact with your app for a period exceeding this timeout, the token dies.
For example, Auth0 defaults to a 30-day idle lifetime. Microsoft uses 90 days for non-SPA applications. If your provider has a shorter idle timeout configured, users who do not use your app daily will experience unexpected logouts.
Each successful token refresh resets the idle timer. So if your app refreshes tokens proactively in the background, the idle timeout is less likely to trigger. But if the app only refreshes on user interaction, any gap longer than the idle window will force re-authentication.
3. Maximum Token Lifetime Reached
The maximum refresh token lifetime is an absolute cap on how long a refresh token can exist, regardless of activity. Even if the user is actively refreshing every hour, the token dies when it hits the maximum age.
Auth0 defaults to a 365-day maximum lifetime. Microsoft allows configurable limits. Some providers set this limit low for security reasons. When a token hits the maximum lifetime, the only option is to send the user through the full authentication flow again.
If your users are getting logged out after a fixed period regardless of activity, check the maximum lifetime setting in your OAuth provider dashboard.
4. Race Conditions from Concurrent Refresh Requests
This is a sneaky problem that hits production apps under load. When an access token expires and multiple API calls happen simultaneously, each call may independently try to refresh the token. If your provider uses token rotation, the first refresh call gets a new token and invalidates the old one.
The second, third, and subsequent refresh calls use the now-invalidated old token. They fail with an invalid_grant or BAD_REFRESH_TOKEN error. To the application, it looks like the refresh token suddenly stopped working.
The NextAuth.js GitHub discussion (issue #3940) documents this extensively. Developers report that concurrent refresh requests in single-page applications cause cascading token failures. The fix involves request queuing or mutex locking, which we cover in the troubleshooting section.
5. SPA and Third-Party Cookie Restrictions
If you are building a single-page application, browser privacy features may be silently killing your refresh tokens. Microsoft’s identity platform limits SPA refresh tokens to just 24 hours when third-party cookies are blocked.
Safari blocks third-party cookies by default. Firefox offers strict tracking protection. Chrome has been phasing out third-party cookies. When your SPA relies on a redirect-based token refresh that involves a cross-origin iframe, these browsers can block the silent refresh attempt.
The result is that SPA users get logged out after exactly 24 hours, even if your token lifetime is configured for 30 days or more. This is not a bug in your code. It is a browser-level restriction that requires architectural changes to work around.
6. Provider-Specific Lifetime Policies
Different OAuth providers have wildly different default refresh token lifetimes. Zoom’s free tier expires refresh tokens after just a few hours. Intuit allows up to 100 days. Google tokens can last up to a year but may expire within 7 days if the app is in testing mode or has not been verified.
If you are integrating with a third-party API and your refresh tokens seem to expire far sooner than documented, check the provider’s specific policy. Account tier, app verification status, and API version can all affect token lifetime.
This is especially common with Zoom. Developers on the Zoom DevForum report that free-tier OAuth apps have refresh tokens that expire after a few hours, while paid tiers get longer lifetimes. The documentation may not make this distinction clear.
7. Token Storage and Transmission Issues
Sometimes the token is fine but the way you store or transmit it causes problems. Storing refresh tokens in browser localStorage exposes them to XSS attacks and can lead to silent corruption. Using HTTP instead of HTTPS can cause some providers to reject refresh requests. Sending the token to the wrong endpoint or with incorrect headers produces errors that look like expiry.
Verify that your token storage mechanism preserves the token exactly as received. Check that your HTTP client is not stripping or modifying authorization headers. These infrastructure issues can masquerade as token expiry when the real problem is in your plumbing.
How to Fix a Refresh Token That Keeps Expiring Too Soon: Step-by-Step
Now that you understand the root causes, let us walk through each fix in order of likelihood. Start at the top and work down until you find the issue affecting your application.
Step 1: Update Your Stored Refresh Token After Every Exchange
This is the first thing to check, and it fixes the majority of premature expiry issues. When you send a refresh token to the token endpoint, the response contains a new access token and, with many providers, a new refresh token.
Your code must extract the new refresh token from the response and update it in your database. Here is a simplified Node.js example:
// Node.js example: updating stored refresh token
async function refreshAccessToken(userId) {
const user = await db.findUser(userId);
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: user.refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
})
});
const tokens = await response.json();
// CRITICAL: Store the new refresh token if one is returned
if (tokens.refresh_token) {
await db.updateRefreshToken(userId, tokens.refresh_token);
}
await db.updateAccessToken(userId, tokens.access_token, tokens.expires_in);
return tokens.access_token;
}
If you skip the if (tokens.refresh_token) block, your old refresh token will eventually be rejected. This applies to Google OAuth, Microsoft identity platform, and any provider that implements refresh token rotation.
In Python with Flask, the same pattern looks like this:
# Python/Flask example: updating stored refresh token
import requests
from flask import current_app
def refresh_access_token(user_id):
user = db.get_user(user_id)
response = requests.post(TOKEN_ENDPOINT, data={
'grant_type': 'refresh_token',
'refresh_token': user.refresh_token,
'client_id': current_app.config['CLIENT_ID'],
'client_secret': current_app.config['CLIENT_SECRET']
})
tokens = response.json()
# Store the new refresh token if returned
if 'refresh_token' in tokens:
db.update_refresh_token(user_id, tokens['refresh_token'])
db.update_access_token(user_id, tokens['access_token'], tokens['expires_in'])
return tokens['access_token']
After implementing this fix, deploy to a test environment and verify that each refresh cycle persists the new token. Monitor your logs for invalid_grant errors. If they disappear, this was your root cause.
Step 2: Check and Adjust Idle and Maximum Token Lifetimes
If updating the stored token does not solve the problem, your next step is to review the token lifetime settings in your OAuth provider dashboard. Most providers expose two settings: idle lifetime and maximum lifetime.
For Auth0, navigate to Applications, then Settings, then Advanced Settings, and look for Refresh Token Expiration. You will see two fields: Refresh Token Absolute Expiration (maximum lifetime) and Refresh Token Idle Expiration (idle lifetime). Both accept values in seconds.
The Auth0 defaults are 2,592,000 seconds (30 days) for idle expiration and 31,536,000 seconds (365 days) for absolute expiration. If your application requires longer sessions, you can increase these values. Setting the idle lifetime to 0 or leaving infinite token lifetime enabled will disable idle expiration entirely.
For Microsoft identity platform, token lifetimes are configured through Conditional Access policies in the Microsoft Entra admin center. Non-SPA applications default to 90 days. SPA applications are capped at 24 hours when third-party cookies are blocked.
After changing lifetime settings, existing tokens are not retroactively affected. New tokens issued after the change will use the new lifetimes. You may need to have users re-authenticate once to get tokens with the updated configuration.
Step 3: Enable Refresh Token Rotation
Refresh token rotation issues a new refresh token on every use and invalidates the previous one. This improves security by limiting the window of opportunity for token theft. It also helps with premature expiry detection.
With rotation enabled, each refresh call returns a new token. If your code correctly stores the new token (Step 1), rotation extends the session indefinitely within the maximum lifetime. Without rotation, the same token is reused until it hits its expiry timestamp.
Auth0 supports rotation with automatic reuse detection. When a previously used refresh token is submitted again, Auth0 detects the reuse and immediately revokes the entire token family. This is a security feature, but it can also cause unexpected logouts if your application accidentally submits an old token.
To enable rotation in Auth0, go to Applications, select your application, then Settings, then Refresh Token settings. Toggle on Refresh Token Rotation. You can also enable Refresh Token Absolute Lifetime and set a value that fits your session requirements.
If you are building a custom OAuth server, implement rotation by generating a new refresh token on each refresh grant, storing it, and marking the old one as used. Add reuse detection by flagging any attempt to use an already-used token as a security event and revoking all tokens for that session.
Step 4: Prevent Race Conditions with Request Queuing
If your logs show intermittent invalid_grant errors that happen under load, you likely have a race condition. Multiple concurrent requests try to refresh the token simultaneously, and only the first one succeeds.
The fix is to serialize refresh requests using a promise-based queue or mutex lock. Here is a Node.js pattern that ensures only one refresh happens at a time:
// Node.js: Mutex lock for refresh token requests
let refreshPromise = null;
async function refreshAccessTokenSafely(userId) {
// If a refresh is already in progress, wait for it
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = refreshAccessToken(userId)
.finally(() => {
refreshPromise = null;
});
return refreshPromise;
}
This pattern ensures that when the first refresh request starts, any subsequent requests during that window wait for the same promise to resolve. They all get the same new access token. After the refresh completes, the lock is released.
For distributed systems running across multiple servers, a local mutex is not enough. Use a distributed lock with Redis or a similar store. The pattern is the same: acquire a lock before refreshing, release it after. Other instances wait for the lock before attempting their own refresh.
One developer on the NextAuth GitHub discussion described their production fix: “We implemented a refresh token queue using Redis. All instances check Redis for an in-progress refresh before attempting their own. This eliminated 99 percent of our invalid_grant errors.”
Step 5: Address SPA 24-Hour Token Expiry
If your single-page application users get logged out after exactly 24 hours, third-party cookie blocking is the likely culprit. The solution depends on your architecture.
Option A: Use a backend-for-frontend (BFF) pattern. Move token refresh logic to your backend server. The SPA communicates with your server via HTTP-only cookies, and the server handles OAuth token management. This eliminates cross-origin token refreshes entirely.
Option B: If you are using Microsoft identity platform, register your redirect URI as a “Web” application type instead of “SPA.” This changes how tokens are delivered and avoids the 24-hour restriction. However, it also means you lose silent refresh via hidden iframe.
Option C: Use first-party cookies instead of relying on third-party cookie behavior. Ensure your authentication domain matches your application domain, or use a subdomain structure that browsers treat as first-party.
Each approach has trade-offs. The BFF pattern is the most robust long-term solution and aligns with current OAuth best practices for browser-based applications.
Step 6: Verify Provider-Specific Token Policies
Some token expiry issues are not bugs in your code but intentional provider behavior. Before spending more time debugging, confirm what your provider’s actual refresh token lifetime policy is.
For Google OAuth, tokens can last up to 6 months to a year in production. However, if your app is in “Testing” status, refresh tokens expire after 7 days. Move your app to “In Production” in the Google Cloud Console to resolve this. Also, Google may expire tokens if the user account is inactive for 6 months or if the user changes their password.
For Zoom, free-tier OAuth apps have refresh tokens that expire after a few hours. Upgrading to a paid account extends the lifetime. There is no code fix for this. You need to inform users or upgrade your Zoom plan.
For Intuit, refresh tokens last up to 100 days. If yours expire sooner, check whether your app is making unnecessary revocation calls or whether the user has revoked access from their account settings.
Step 7: Debugging Checklist for Refresh Token Expiry
If none of the above steps resolved your issue, work through this systematic checklist. Each item rules out a specific category of problem:
- Log the exact error response from the token endpoint. Is it
invalid_grant,invalid_request, or something else? The error code tells you whether the token is expired, revoked, or malformed. - Log the timestamp of each refresh attempt. Compare it to the token issuance time. Is the gap shorter than your configured lifetime?
- Check whether the refresh token value changes between issuance and use. If it does, your storage or retrieval logic has a bug.
- Verify that your token endpoint URL is correct for your provider. Using the wrong endpoint returns errors that look like expiry.
- Confirm that your client ID and secret match the OAuth app registration. Mismatched credentials cause
invalid_clienterrors that some libraries surface as token failures. - Check for clock skew between your server and the OAuth provider. Tokens with
nbf(not-before) claims can be rejected if your server time is behind the provider’s clock. - Inspect your HTTP client for automatic retry logic that might be re-submitting used tokens.
- Review your load balancer and CDN settings for request buffering or duplication that could cause double-submission.
- Check whether a background job or health check is making refresh calls with stale tokens, invalidating the active token family.
- Test with a fresh user session to rule out issues with a specific token being corrupted.
Going through this checklist methodically will narrow down the problem. Most production issues are caught by the first three items on this list.
Configuring Refresh Token Lifetimes by Provider
Configuration differs significantly across OAuth providers. Below are the key settings for the most common platforms.
Auth0 Refresh Token Configuration
Auth0 provides two configurable lifetime settings accessible through the Dashboard or the Management API. The idle expiration setting controls how long a token can go unused. The absolute expiration setting caps the total token age regardless of activity.
To configure via the Dashboard, navigate to Applications, select your application, and scroll to Advanced Settings. Enable Refresh Token Rotation for improved security. Set Refresh Token Absolute Expiration to your desired maximum session length. The idle lifetime can be set independently and resets on each refresh.
Alternatively, use the Management API to update these settings programmatically:
PATCH /api/v2/clients/{client_id}
Content-Type: application/json
{
"refresh_token": {
"rotation_type": "rotating",
"expiration_type": "expiring",
"leeway": 0,
"token_lifetime": 2592000,
"infinite_token_lifetime": false,
"infinite_idle_token_lifetime": false,
"idle_token_lifetime": 2592000
}
}
Setting rotation_type to “rotating” enables refresh token rotation. Each refresh returns a new token and invalidates the old one. Combined with the idle_token_lifetime of 2,592,000 seconds (30 days), this configuration supports long-lived sessions with strong security.
Microsoft Identity Platform Configuration
Microsoft Entra ID (formerly Azure AD) manages token lifetimes through Conditional Access policies. The defaults are 90 days for confidential clients and non-SPA public clients, and 24 hours for SPAs when third-party cookies are blocked.
To adjust these values, sign in to the Microsoft Entra admin center. Navigate to Protection, then Conditional Access. Create or edit a policy that targets your application and configure the session controls. Note that Microsoft has been moving toward fixed lifetimes, and some configurations may no longer be customizable.
Microsoft’s documentation also notes that refresh tokens are replaced on each use. The new token carries a fresh validity window. This means that active users effectively never experience token expiry under normal conditions, as long as the application correctly stores and uses the replacement token.
Google OAuth Configuration
Google does not expose refresh token lifetime settings in a dashboard. The token lifetime is determined by Google’s internal policies. In production mode, refresh tokens can last for up to 6 months or longer. In testing mode, they expire after 7 days.
To move your app from testing to production, go to the Google Cloud Console. Navigate to APIs and Services, then OAuth consent screen. Click “Publish App” to move it from Testing to In Production. Once published, refresh tokens will follow the production lifetime policy.
Google returns a new refresh token on some (but not all) refresh exchanges. Your code must check for a refresh_token field in the response and update your stored value whenever one is present. Failing to do this is the most common cause of Google refresh token expiry issues.
Okta and Other Providers
Okta allows refresh token lifetime configuration through the authorization server policy. Navigate to Security, then API, then select your authorization server. Edit the policy rules to set the refresh token lifetime and rotation behavior.
Most providers follow a similar pattern: look for policy or application settings related to tokens or sessions. The specific UI varies, but the concepts (idle lifetime, maximum lifetime, rotation) are consistent across the OAuth 2.0 ecosystem.
Provider-Specific Refresh Token Policies
Understanding each provider’s default behavior helps you set expectations and avoid surprises. Below is a comparison of the most commonly integrated OAuth providers and their refresh token policies.
Refresh Token Lifetime Defaults by Provider
The following defaults are based on official documentation and community-reported values as of 2026. Actual lifetimes may vary based on account tier, app configuration, and API version:
- Auth0: 30 days idle (default), 365 days maximum (default). Configurable via Dashboard or Management API.
- Microsoft Entra ID: 90 days for confidential and non-SPA public clients. 24 hours for SPAs with blocked third-party cookies.
- Google OAuth: Up to 6 months or more in production. 7 days in testing mode. New tokens issued on some refreshes.
- Zoom: A few hours for free-tier accounts. Longer for paid tiers. Not user-configurable.
- Intuit: 100 days maximum. Not configurable.
- Okta: Configurable through authorization server policies. Defaults vary by org setup.
- HubSpot: Effectively non-expiring unless the user uninstalls the app or revokes access.
This comparison highlights why a one-size-fits-all approach does not work. An integration that works fine with Auth0’s 30-day idle lifetime will fail immediately with Zoom’s few-hour lifetime if you do not account for the difference.
Common Provider-Specific Gotchas
Beyond default lifetimes, each provider has quirks that can cause unexpected token behaviour:
Google’s testing mode is the most common gotcha. Developers see refresh tokens expiring within 7 days and assume their code is broken. Publishing the app resolves this immediately.
Microsoft’s SPA 24-hour limit catches many developers off guard. The token works fine during development (where cookies are allowed) but fails in production behind Safari or Firefox strict mode.
Auth0’s reuse detection is a powerful security feature that can cause unexpected logouts if your code accidentally submits an old token. This happens most often with race conditions or when background processes hold stale references.
Zoom’s tier-based lifetime restriction is undocumented in some API versions. Developers discover it only when their free-tier integration stops working after a few hours.
Preventing Refresh Token Expiry: Best Practices
Once you have fixed your immediate issue, follow these best practices to prevent future problems:
- Always store the new refresh token: Every time you exchange a refresh token, check the response for a new one and persist it immediately.
- Use refresh token rotation: It improves security and makes token theft much harder to exploit. Just make sure your code handles the rotation correctly.
- Implement request queuing: Serialize refresh requests to prevent race conditions. This is critical for any application with concurrent API calls.
- Store tokens securely: Keep refresh tokens in an encrypted database or secure HTTP-only cookies. Never store them in localStorage.
- Always use HTTPS: Token transmission over HTTP can be intercepted. Some providers reject refresh requests over insecure connections.
- Monitor for error patterns: Set up alerts for
invalid_grantandBAD_REFRESH_TOKENerrors. Spikes in these errors indicate token management issues. - Implement graceful re-authentication: When a refresh token truly expires, redirect the user smoothly to login instead of showing a raw error page.
- Use short-lived access tokens: Pair 15-60 minute access tokens with long-lived refresh tokens. This minimizes the window for access token theft while maintaining user experience.
- Test under concurrent load: Simulate multiple simultaneous refresh requests to verify your race condition fix holds up.
- Document your token lifecycle: Record which provider you use, the configured lifetimes, and the expected behaviour so your team can debug faster in the future.
Following these practices will keep your authentication system stable and your users logged in for the expected duration.
Testing Refresh Token Expiry Without Waiting
One challenge developers face is testing refresh token behaviour without waiting hours or days for real expiry. Here are practical strategies:
Most providers allow you to set short token lifetimes in a development or test environment. Set your idle lifetime to 5 minutes and your maximum lifetime to 15 minutes during testing. This lets you observe the full lifecycle quickly.
You can also manually revoke a token through your provider’s dashboard or API to simulate expiry. Auth0 provides a revocation endpoint. Google has a token revocation endpoint at https://oauth2.googleapis.com/revoke. Microsoft offers revocation through the Microsoft Graph API.
To test race conditions, write a script that fires multiple concurrent refresh requests simultaneously. If your queue or mutex is working correctly, all requests should succeed. Without it, you will see invalid_grant errors on the second and subsequent requests.
For SPA cookie issues, test in Safari with Intelligent Tracking Prevention enabled. If your tokens expire after 24 hours in Safari but not in Chrome, third-party cookie blocking is the cause.
FAQ’s
What to do when a refresh token expires?
When a refresh token expires, redirect the user to re-authenticate through the full OAuth authorization flow. To prevent frequent expiry, ensure your backend stores the new refresh token returned on each refresh cycle, configure appropriate idle and maximum lifetime values in your provider settings, and enable refresh token rotation with reuse detection.
How do I fix a token expiration issue?
To fix a token expiration issue, follow these steps: (1) Update your backend to store the new refresh token returned on each exchange. (2) Check and adjust idle and maximum token lifetimes in your OAuth provider settings. (3) Enable refresh token rotation. (4) Implement request queuing to prevent race conditions. (5) Check for third-party cookie issues if using a single-page application.
How to fix the refresh token has expired due to inactivity?
To fix a refresh token expired due to inactivity, increase the idle refresh token lifetime in your OAuth provider settings. The idle lifetime controls how long a token can remain unused before expiring. Each successful token refresh resets the idle timer. Set a longer idle period (30 days or more) if users should not need to re-authenticate frequently.
How long should a refresh token last?
A refresh token should typically last between 7 and 90 days depending on your security requirements. Auth0 defaults to 30 days idle and 365 days maximum. Microsoft uses 90 days for non-SPA apps. Google allows up to 6 months in production mode. Balance user convenience against security risk when choosing your lifetime.
Why does my refresh token keep expiring?
Your refresh token likely keeps expiring because your backend is not storing the new refresh token returned on each exchange. Many providers including Google issue a new refresh token on every refresh. If you keep using the old one, it gets rejected. Other common causes include short idle timeout settings, race conditions from concurrent requests, and provider-specific policies like Google testing mode expiring tokens after 7 days.
How to prevent refresh token from expiring?
To prevent refresh tokens from expiring prematurely, always update the stored refresh token after each exchange, set appropriate idle and maximum lifetimes, enable refresh token rotation, implement request queuing to avoid race conditions, and use a BFF pattern for SPAs to avoid third-party cookie issues. For Google apps, move from testing to production mode to extend token lifetimes beyond 7 days.
Conclusion
Learning how to fix a refresh token that keeps expiring too soon comes down to understanding the token lifecycle in your specific OAuth setup. The most common fix, storing the new refresh token returned on each exchange, resolves the majority of cases we see in the wild.
For the remaining cases, checking lifetime configurations, enabling rotation, preventing race conditions, and accounting for SPA browser restrictions will cover nearly everything else. Start with Step 1 in our troubleshooting guide and work through the steps methodically.
If you found this guide helpful, bookmark it for future reference. Token expiry issues tend to resurface when you change providers, add new integrations, or scale your user base. Having a systematic troubleshooting process will save you and your team significant time.