Keycloak Token Expired: Understanding and Fixing Token Issues
Last updated: March 2026
Keycloak access tokens expire after 5 minutes by default, and refresh tokens expire when the SSO or client session is idle for more than 30 minutes — or reaches its absolute maximum of 10 hours. A “token expired” error in your application means either the access token was not refreshed in time, the refresh token itself has expired and the user must re-authenticate, or there is a clock skew between the Keycloak server and your API server large enough to make valid tokens appear expired.
This guide breaks down every timeout that affects token validity, explains how they interact, and provides concrete fixes for the most common scenarios.
How Keycloak Tokens Work
Keycloak issues three types of tokens during an OpenID Connect authentication flow:
- Access Token: A short-lived JWT that the client presents to resource servers. It contains user claims, roles, and scopes. Default lifetime: 5 minutes.
- Refresh Token: A longer-lived opaque token used to obtain new access tokens without requiring the user to re-authenticate. Default lifetime: 30 minutes.
- ID Token: A JWT containing identity claims about the authenticated user. Used by the client application, not sent to APIs.
You can decode and inspect any of these tokens using the JWT Token Analyzer to see their expiration claims (exp, iat), issued realm, and embedded roles.
The typical token flow looks like this:
- User authenticates with Keycloak and receives all three tokens.
- The application uses the access token for API calls.
- When the access token expires, the application uses the refresh token to get a new access token.
- When the refresh token expires, the user must re-authenticate.
The Five Timeout Settings That Matter
Keycloak has several timeout values that interact to determine how long a user stays authenticated. Understanding each one is essential. The official Keycloak session documentation covers these in detail.
1. Access Token Lifespan
Location: Realm Settings > Tokens > Access Token Lifespan
This controls how long the access token JWT is valid. The exp claim in the token is set to iat + access_token_lifespan.
Default: 5 minutes
Access Token Lifespan = 300 seconds (5 minutes)
A short lifespan limits the damage window if a token is compromised. The trade-off is more frequent token refresh requests.
2. SSO Session Idle Timeout
Location: Realm Settings > Sessions > SSO Session Idle
This is the maximum time a user’s SSO session can remain idle (no token refresh or new authentication) before it expires. When this timeout is reached, all refresh tokens associated with the session become invalid.
Default: 30 minutes
3. SSO Session Max Lifespan
Location: Realm Settings > Sessions > SSO Session Max
The absolute maximum duration of an SSO session, regardless of activity. Even if the user is actively refreshing tokens, the session will expire after this period.
Default: 10 hours
4. Client Session Idle Timeout
Location: Realm Settings > Tokens > Client Session Idle
This is the idle timeout for a specific client’s session within the broader SSO session. It can be shorter than the SSO Session Idle but never longer.
Default: Inherits from SSO Session Idle
5. Client Session Max Lifespan
Location: Realm Settings > Tokens > Client Session Max
The maximum lifespan for a specific client’s session. Like the client idle timeout, it can be shorter than the SSO Session Max but never longer.
Default: Inherits from SSO Session Max
For a comprehensive walkthrough of session timeout best practices, see the companion post on Keycloak session timeout configuration.
How Timeouts Interact
The effective lifetime of a refresh token is determined by the minimum of these values:
Refresh Token Lifetime = min(
SSO Session Idle,
SSO Session Max - elapsed time,
Client Session Idle,
Client Session Max - elapsed time
)
This means that even if your SSO Session Idle is 30 minutes, a Client Session Idle of 10 minutes will cause the refresh token to expire after 10 minutes of inactivity.
Here is a practical example:
| Setting | Value |
|---|---|
| Access Token Lifespan | 5 minutes |
| SSO Session Idle | 30 minutes |
| SSO Session Max | 10 hours |
| Client Session Idle | 15 minutes |
| Client Session Max | 2 hours |
In this configuration:
- The access token is valid for 5 minutes.
- If the user is idle for 15 minutes, the refresh token expires (Client Session Idle).
- Even with continuous activity, the client session ends after 2 hours.
- The SSO session itself can last up to 10 hours if the user is active.
Common Token Expiration Scenarios and Fixes
Scenario 1: Access Token Expired During API Call
Symptom: API returns 401 Unauthorized with Token is not active or Token expired.
Fix: Implement token refresh logic in your application. The access token is intentionally short-lived. Your application should refresh it proactively.
// Token refresh middleware for Axios
import axios from 'axios';
let isRefreshing = false;
let refreshSubscribers = [];
function onRefreshed(newToken) {
refreshSubscribers.forEach((callback) => callback(newToken));
refreshSubscribers = [];
}
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve) => {
refreshSubscribers.push((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(axios(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const newToken = await refreshAccessToken();
isRefreshing = false;
onRefreshed(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return axios(originalRequest);
} catch (refreshError) {
isRefreshing = false;
// Redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
async function refreshAccessToken() {
const response = await axios.post(
'https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token',
new URLSearchParams({
grant_type: 'refresh_token',
client_id: 'my-client',
refresh_token: getStoredRefreshToken(),
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
storeTokens(response.data);
return response.data.access_token;
}
Scenario 2: Refresh Token Expired
Symptom: Token refresh request returns 400 Bad Request with Session not active or Token is not active.
Fix: This means the SSO or client session has expired. The user must re-authenticate. Check your idle and max timeout values and adjust them based on your use case.
To increase the refresh token lifetime:
- Go to Realm Settings > Sessions.
- Increase SSO Session Idle (e.g., from 30 minutes to 8 hours for a SaaS application).
- Verify that Client Session Idle is not overriding this with a lower value.
Scenario 3: Clock Skew Between Services
Symptom: Tokens appear to expire before they should, or freshly issued tokens are rejected as expired.
Fix: Clock skew occurs when the Keycloak server and the resource server have different system times. Even a 30-second drift can cause problems with a 5-minute access token.
# Check time on your Keycloak server
date -u
# Check time on your API server
date -u
# If using NTP, force sync
sudo ntpdate -u pool.ntp.org
Most JWT validation libraries allow configuring a clock skew tolerance:
// Java with jose4j
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setAllowedClockSkewInSeconds(30)
.setRequireExpirationTime()
.setExpectedIssuer("https://keycloak.example.com/realms/my-realm")
.setVerificationKey(publicKey)
.build();
# Python with PyJWT
import jwt
decoded = jwt.decode(
token,
key=public_key,
algorithms=["RS256"],
leeway=30, # 30 seconds clock skew tolerance
audience="my-client",
)
Scenario 4: Offline Tokens Not Working
Symptom: Offline tokens expire unexpectedly.
Fix: Offline tokens are special refresh tokens that survive server restarts and session cleanup. They have their own timeout settings:
- Go to Realm Settings > Tokens.
- Set Offline Session Idle (default: 30 days).
- Optionally enable Offline Session Max Limited and set a maximum lifespan.
The client must request the offline_access scope to receive an offline token:
curl -X POST "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token"
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=password"
-d "client_id=my-client"
-d "username=user"
-d "password=pass"
-d "scope=openid offline_access"
Token Refresh Strategies by Language
Python
import requests
import time
class KeycloakTokenManager:
def __init__(self, server_url, realm, client_id, client_secret=None):
self.token_url = f"{server_url}/realms/{realm}/protocol/openid-connect/token"
self.client_id = client_id
self.client_secret = client_secret
self.tokens = None
self.expires_at = 0
def get_access_token(self):
if self.tokens and time.time() < self.expires_at - 30:
return self.tokens["access_token"]
if self.tokens and "refresh_token" in self.tokens:
return self._refresh()
raise Exception("No valid token available. Re-authenticate.")
def _refresh(self):
data = {
"grant_type": "refresh_token",
"client_id": self.client_id,
"refresh_token": self.tokens["refresh_token"],
}
if self.client_secret:
data["client_secret"] = self.client_secret
response = requests.post(self.token_url, data=data)
if response.status_code != 200:
raise Exception(f"Token refresh failed: {response.json()}")
self.tokens = response.json()
self.expires_at = time.time() + self.tokens["expires_in"]
return self.tokens["access_token"]
Go
package auth
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
type TokenManager struct {
tokenURL string
clientID string
clientSecret string
tokens *TokenResponse
expiresAt time.Time
mu sync.Mutex
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
func (tm *TokenManager) GetAccessToken() (string, error) {
tm.mu.Lock()
defer tm.mu.Unlock()
if tm.tokens != nil && time.Now().Before(tm.expiresAt.Add(-30*time.Second)) {
return tm.tokens.AccessToken, nil
}
if tm.tokens != nil && tm.tokens.RefreshToken != "" {
return tm.refresh()
}
return "", fmt.Errorf("no valid token, re-authenticate")
}
func (tm *TokenManager) refresh() (string, error) {
data := url.Values{
"grant_type": {"refresh_token"},
"client_id": {tm.clientID},
"client_secret": {tm.clientSecret},
"refresh_token": {tm.tokens.RefreshToken},
}
resp, err := http.Post(tm.tokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return "", err
}
defer resp.Body.Close()
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", err
}
tm.tokens = &tokenResp
tm.expiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
return tokenResp.AccessToken, nil
}
Recommended Timeout Values by Use Case
| Use Case | Access Token | SSO Session Idle | SSO Session Max | Notes |
|---|---|---|---|---|
| Banking / Financial | 2 min | 5 min | 30 min | Strict for compliance |
| SaaS Application | 5 min | 30 min | 12 hours | Balance UX and security |
| E-commerce | 5 min | 1 hour | 24 hours | Longer to avoid cart abandonment |
| Internal Tools | 10 min | 4 hours | 12 hours | Convenience for employees |
| Mobile App | 5 min | 30 days (offline) | 90 days | Use offline tokens |
For managed Keycloak with pre-configured session policies, see the Session Management feature page. You can also review how audit logs help track session events across your realm.
Debugging Token Expiry Issues
When you are unsure why a token was rejected, follow this process:
-
Decode the token: Use the JWT Token Analyzer to inspect the
exp,iat, andauth_timeclaims. -
Check the Keycloak event log: Go to Events > Login Events and look for
REFRESH_TOKENandREFRESH_TOKEN_ERRORevents. The error details will tell you exactly which timeout was exceeded. -
Compare timestamps: Check whether the token’s
expclaim matches what you expect based on your realm settings. -
Verify realm settings via API:
# Get realm configuration (requires admin access)
curl -s "https://keycloak.example.com/admin/realms/my-realm"
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{
accessTokenLifespan,
ssoSessionIdleTimeout,
ssoSessionMaxLifespan,
clientSessionIdleTimeout,
clientSessionMaxLifespan,
offlineSessionIdleTimeout,
offlineSessionMaxLifespan
}'
This outputs all relevant timeout values in one view, making it easy to identify misconfigurations.
For more on Keycloak token validation patterns, see our post on Keycloak token validation for APIs and the guide to JWT token lifecycle management.
Client-Level Token Overrides
Individual clients can override realm-level token settings. This is useful when different applications need different token lifetimes.
To configure per-client overrides:
- Go to Clients > your-client > Advanced Settings.
- Set Access Token Lifespan to override the realm default.
- Set Client Session Idle and Client Session Max for client-specific session limits.
This is particularly useful in multi-tenant setups where different tenants have different security requirements. For a deeper dive into multi-tenancy, see our post on multitenancy in Keycloak using the organizations feature.
Preventing Token Issues in Production
Follow these guidelines to minimize token-related issues:
-
Always implement token refresh logic. Never assume the access token is valid. Check the
expclaim or catch 401 responses and refresh proactively. -
Monitor token refresh failures. Log refresh failures and alert on spikes, which could indicate session configuration problems or Keycloak issues.
-
Use appropriate token lifetimes. A 5-minute access token with a 30-minute refresh token is a reasonable default for most web applications.
-
Sync clocks with NTP. Ensure all servers (Keycloak, API servers, clients) use NTP for time synchronization.
-
Test timeout behavior. During development, temporarily set short timeout values (e.g., 1-minute access tokens) to verify your refresh logic works correctly.
Wrapping Up
Token expiration in Keycloak is governed by a hierarchy of timeout settings at the realm and client level. Most “token expired” issues come down to either missing refresh logic in the application or a mismatch between session timeout values and user expectations.
Start by inspecting the token with the JWT Token Analyzer, check the Keycloak event log, and verify your realm timeout configuration. With the right settings and proper refresh logic, token expiration becomes invisible to your users.
Frequently asked questions
Why does my Keycloak access token expire after 5 minutes?
5 minutes is the default Access Token Lifespan in Keycloak. This is intentional: a short lifespan limits the damage window if a token is intercepted. Your application should use the refresh token to obtain a new access token when the current one expires, rather than increasing this value. You can change the default in Realm Settings > Tokens > Access Token Lifespan, or override it per client under Clients > your client > Advanced Settings.
How do I stop users from being logged out after 30 minutes in Keycloak?
The 30-minute idle logout is controlled by SSO Session Idle in Realm Settings > Sessions. Increasing it (for example, to 8 hours for a SaaS application) keeps users active as long as they interact with the application within that window. Make sure Client Session Idle for the specific client is not set to a lower value that overrides the realm setting. The effective refresh token lifetime is the minimum of both the SSO and client idle timeouts.
What is the difference between SSO Session Idle and SSO Session Max in Keycloak?
SSO Session Idle is the inactivity timeout — the session expires if no token refresh occurs within this period. SSO Session Max is the absolute maximum session lifetime regardless of activity. Even if a user refreshes tokens continuously, the session will end after the max lifespan is reached. For most web applications, set SSO Session Idle to match expected user inactivity (30 minutes to several hours) and SSO Session Max to the longest reasonable working day (8-12 hours).
How do I fix “Token is not active” caused by clock skew between Keycloak and my API?
If the clocks on your Keycloak server and your API server differ by more than a few seconds, valid tokens can appear expired. Run date -u on both hosts to check for drift, then ensure all servers synchronize via NTP (timedatectl status on Linux). Most JWT validation libraries also support a configurable skew tolerance — for example, leeway=30 in PyJWT or setAllowedClockSkewInSeconds(30) in jose4j — which absorbs small clock differences without requiring perfect synchronization.
How do I use Keycloak offline tokens for long-lived sessions in mobile apps?
Request the offline_access scope during authentication to receive an offline refresh token. Offline tokens survive Keycloak server restarts and session cleanup and follow their own timeout settings under Realm Settings > Tokens > Offline Session Idle (default: 30 days) and Offline Session Max Lifespan (optional). The client must also have the offline_access scope enabled. Use offline tokens for mobile apps where users expect to stay logged in for weeks without re-authenticating.
If you want to avoid managing these configurations yourself, Skycloak offers managed Keycloak hosting with expert-configured session policies, built-in monitoring, and 24/7 support — letting you focus on building your application instead of debugging token issues.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.