Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This article helps you diagnose and resolve token cache problems in Microsoft.Identity.Web. Token cache issues can cause authentication failures, degraded performance, or unexpected sign-in prompts. For an overview of how token caching works in Microsoft.Identity.Web, see Token cache overview.
Prerequisites
Before you troubleshoot, confirm the following:
- You're using a supported version of Microsoft.Identity.Web.
- Your application has token caching configured in
Program.csorStartup.cs. - You have access to application logs and, if applicable, your distributed cache infrastructure.
Enable token cache logging and diagnostics
Enable detailed logging as your first diagnostic step. Microsoft.Identity.Web uses the ASP.NET Core logging infrastructure and emits events through the Microsoft Authentication Library (MSAL).
Enable MSAL logging
Set the log level to Debug for identity libraries in your appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Identity.Web": "Debug",
"Microsoft.IdentityModel": "Debug"
}
}
}
Subscribe to MSAL cache events
Subscribe to MSAL token cache notification events to track cache hits, misses, and serialization activity:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.OnL2CacheFailure = (ex) =>
{
logger.LogWarning(ex, "L2 cache failure encountered.");
// Return true to allow the operation to continue despite the cache failure.
// Return false to propagate the exception.
return true;
};
});
Monitor cache metrics
For production monitoring, track these key metrics:
- Cache hit rate — a low hit rate indicates tokens aren't being retrieved from cache.
- L2 cache latency — high latency suggests a distributed cache connectivity or performance issue.
- Cache serialization errors — errors during read or write indicate corruption or version mismatch.
- Memory consumption — sustained growth can indicate missing eviction policies.
Distributed cache (L2) connection failures
Symptom
Application logs show connection timeout errors or intermittent authentication failures. Users experience sign-in delays, and you see exceptions such as:
Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache:
StackExchange.Redis.RedisConnectionException:
No connection is active/available to service this operation.
Or for SQL Server distributed cache:
Microsoft.Data.SqlClient.SqlException:
A network-related or instance-specific error occurred while
establishing a connection to SQL Server.
Cause
The distributed cache backing store (Redis or SQL Server) is unreachable. Common causes include:
- Incorrect connection string or expired access credentials.
- Network firewall rules blocking the connection from the app host.
- The cache service is down or undergoing maintenance.
- SSL/TLS configuration mismatch between the client and cache server.
Diagnostic steps
Follow these steps to identify the connection failure:
- Verify connectivity. From the application host, test the connection to Redis or SQL Server by using
Test-NetConnection(PowerShell) orredis-cli. - Check the connection string. Confirm the connection string matches the cache server's hostname, port, and credentials.
- Review firewall rules. In Azure, verify that the app service or virtual network can reach the cache resource.
- Check service health. In the Azure portal, review the health and metrics of your Azure Cache for Redis or SQL Database instance.
Solution
Step 1: Correct the connection string
Verify the connection string in your appsettings.json:
{
"ConnectionStrings": {
"Redis": "your-redis-instance.redis.cache.windows.net:6380,password=your-access-key,ssl=True,abortConnect=False"
}
}
Important
Set abortConnect=False in the Redis connection string. This setting allows the application to reconnect automatically after transient connection failures rather than throwing immediately.
Step 2: Configure retry and resilience
Configure the OnL2CacheFailure callback so your application degrades gracefully when the distributed cache is temporarily unavailable:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.OnL2CacheFailure = (ex) =>
{
// Log the failure for monitoring and alerting.
logger.LogWarning(ex, "Distributed token cache is unavailable. " +
"Falling back to in-memory cache.");
return true; // Continue without the L2 cache.
};
// Set a timeout to avoid blocking the request pipeline.
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
});
Step 3: Open firewall rules
If the application runs in Azure App Service and the cache is in a virtual network, add the App Service outbound IP addresses to the cache firewall allow list.
Cache deserialization errors
Symptom
After you upgrade Microsoft.Identity.Web or MSAL.NET, the application throws deserialization exceptions when reading from the distributed cache. Users must sign in again, and you see exceptions such as:
System.Text.Json.JsonException:
The JSON value could not be converted to the expected type.
Or:
Microsoft.Identity.Client.MsalClientException:
Error code: json_parse_failed
Cause
The token cache serialization format changed between library versions. Tokens cached by the previous version can't be deserialized by the new version. This issue occurs most often during major version upgrades of MSAL.NET or Microsoft.Identity.Web.
Solution
Option A: Clear the cache
The simplest fix is to clear all entries in the distributed cache. Users reauthenticate once, and subsequent tokens are written in the new format.
Flush the Redis cache:
redis-cli FLUSHDB
Or clear the SQL Server distributed cache table:
DELETE FROM [dbo].[TokenCache];
Note
Clearing the cache causes all active users to reauthenticate. Plan this operation during a maintenance window if your application serves a large user base.
Option B: Handle deserialization errors gracefully
Configure the cache adapter to treat deserialization failures as cache misses rather than fatal errors:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.OnL2CacheFailure = (ex) =>
{
if (ex is JsonException or MsalClientException)
{
logger.LogWarning(ex, "Cache deserialization failed. " +
"Treating as cache miss.");
return true;
}
return false; // Propagate unexpected errors.
};
});
With this approach, affected cache entries are automatically replaced as users reauthenticate, and no manual cache flush is required.
Encryption key mismatch across servers
Symptom
Deserialization errors occur in multi-instance deployments even though the distributed cache is working. Tokens cached by one server instance can't be read by another. You see json_parse_failed or IDW10802 errors in the logs.
Cause
When cache encryption is enabled (options.Encrypt = true), Microsoft.Identity.Web uses ASP.NET Core Data Protection to encrypt cache entries. By default, each server instance generates its own Data Protection keys, so one instance can't decrypt entries written by another.
Solution
Configure ASP.NET Core Data Protection to share encryption keys across all server instances.
Option A: Azure Blob Storage + Azure Key Vault (recommended for Azure deployments)
using Microsoft.AspNetCore.DataProtection;
using Azure.Identity;
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(
new Uri("https://yourstorageaccount.blob.core.windows.net/dataprotection/keys.xml"),
new DefaultAzureCredential())
.ProtectKeysWithAzureKeyVault(
new Uri("https://yourkeyvault.vault.azure.net/keys/dataprotection-key"),
new DefaultAzureCredential());
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.Encrypt = true;
});
This configuration stores the Data Protection key ring in Azure Blob Storage and protects the keys at rest with Azure Key Vault. All application instances that access the same blob and key can encrypt and decrypt each other's cache entries.
Option B: Shared file system with certificate protection
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\keys"))
.ProtectKeysWithCertificate(certificate);
Tip
When rotating the Data Protection certificate, use UnprotectKeysWithAnyCertificate to include both the current and previous certificates. This allows decryption of keys that were protected with the old certificate during the rotation window.
Memory growth with in-memory cache
Symptom
Application memory consumption grows steadily over time. If the application runs in a container or App Service plan with a fixed memory limit, it eventually restarts or throws OutOfMemoryException. Monitoring shows the managed heap growing without reclamation by garbage collection.
Cause
Using AddInMemoryTokenCaches() without size limits causes unbounded cache growth. This situation is especially problematic in applications that serve many users, because each user's token entry consumes memory indefinitely.
By default, MemoryCache doesn't enforce a maximum size and doesn't evict entries unless an expiration policy is set.
Solution
Option A: Set a size limit and sliding expiration
Configure the in-memory cache with expiration policies:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
services.Configure<MsalMemoryTokenCacheOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
options.SlidingExpiration = TimeSpan.FromHours(2);
});
With these settings, entries expire after 12 hours regardless of access, and entries idle for 2 hours are evicted earlier.
Option B: Switch to a distributed cache
For applications with many concurrent users, an in-memory cache doesn't scale. Switch to a distributed cache such as Redis:
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
});
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
A distributed cache offloads memory from the application process, persists tokens across restarts, and supports multi-instance deployments.
Option C: Use the L1/L2 hybrid architecture
Microsoft.Identity.Web supports a hybrid approach that combines a fast in-memory L1 cache with a persistent distributed L2 cache. Configure the L1/L2 hybrid cache:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.L1CacheOptions = new MsalMemoryTokenCacheOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
SlidingExpiration = TimeSpan.FromMinutes(2)
};
});
With L1/L2 caching, frequently accessed tokens are served from in-memory (L1) with sub-millisecond latency. The L2 cache provides persistence and cross-instance consistency. The L1 cache uses short expirations to limit memory growth.
Repeated MFA or consent prompts
Symptom
Users are repeatedly prompted for multifactor authentication (MFA) or consent even though they completed these steps recently. The application can't find existing tokens in the cache.
Cause
This issue occurs when the token cache lookup fails to match a cached entry to the current user account. Common causes include:
- The cache key differs from the one used when the token was stored. This situation can occur if the
HomeAccountIdor tenant context changes. - The application runs multiple instances behind a load balancer with in-memory caching, and requests route to an instance that doesn't have the user's token.
- The requested claims or scopes changed, so the cached token doesn't satisfy the new requirement.
- Session affinity isn't enabled, so users route to different instances that lack their cached tokens.
Diagnostic steps
Follow these steps to identify why tokens aren't found in the cache:
- Check the cache type. If you use
AddInMemoryTokenCaches()in a multi-instance deployment, tokens cached on one instance aren't available on another. Switch to a distributed cache. - Verify the account identifier. Enable Debug-level logging and search for the
HomeAccountId. Confirm that the identifier is consistent across requests. - Inspect the scopes. Confirm that the scopes requested by
GetAccessTokenForUserAsyncmatch the scopes originally consented to. A scope mismatch causes MSAL to request a new token. - Review Conditional Access policies. A Microsoft Entra ID Conditional Access policy that requires step-up authentication for specific resources causes additional prompts unrelated to caching.
Solution
Step 1: Switch to distributed caching
If your application runs multiple instances, use a distributed cache to share tokens across instances:
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
});
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
Step 2: Verify consistent scopes
Ensure the scopes you request when acquiring tokens match the scopes configured during authentication:
// In authentication setup — initial scopes.
.EnableTokenAcquisitionToCallDownstreamApi(new[] { "User.Read", "Mail.Read" })
// When acquiring a token — use the same scopes.
var token = await tokenAcquisition.GetAccessTokenForUserAsync(
new[] { "User.Read", "Mail.Read" });
Step 3: Enable session affinity (temporary workaround)
If you can't switch to a distributed cache right away, enable session affinity (sticky sessions) on your load balancer. Session affinity routes a user's requests to the same instance. This approach is a temporary workaround with scalability limitations.
Cache performance issues
Symptom
Token retrieval is slow and downstream API calls have increased latency. Monitoring shows high average response times for token acquisition requests. The latency isn't from the identity provider—tokens are served from cache.
Cause
Cache performance issues typically result from:
- High L2 cache latency. The distributed cache is under heavy load, geographically distant from the application, or using an undersized service tier.
- Large token cache entries. Applications that cache tokens for many resources per user can produce large serialized cache entries that are slow to read and write.
- No L1 cache. Every token acquisition goes to the distributed cache over the network, even for tokens that are used frequently.
Solution
Step 1: Enable the L1 in-memory cache
The L1 cache stores frequently accessed tokens in process memory, avoiding network round trips to L2:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.L1CacheOptions = new MsalMemoryTokenCacheOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
SlidingExpiration = TimeSpan.FromMinutes(2)
};
});
With this configuration, tokens served from L1 have sub-millisecond latency. Tokens not in L1 fall back to the L2 distributed cache.
Step 2: Optimize the distributed cache tier
If L2 cache latency is high, consider the following actions:
- Scale up the Redis instance. Move to a higher tier (for example, from Basic to Standard or Premium in Azure Cache for Redis) to get more throughput and lower latency.
- Enable geo-replication. If your application serves users in multiple regions, use Azure Cache for Redis geo-replication so that the cache is close to each region's compute.
- Review network configuration. Use Private Link or VNet integration to reduce network hops between the application and the cache.
Step 3: Reduce serialized token size
If token cache entries are large, review whether the application requests tokens for more resources than necessary. Each unique resource and scope combination adds to the cache entry size. Consolidate API calls where possible to reduce the number of distinct access tokens cached per user.
Redis cache eviction
Symptom
Users are intermittently prompted to reauthenticate with no pattern based on token expiration. Redis monitoring shows evicted_keys increasing and used_memory approaching the maxmemory limit.
Cause
When Redis reaches its maxmemory limit, it evicts keys based on the configured maxmemory-policy. The default policy (volatile-lru) evicts the least recently used keys that have an expiration. If the Redis instance is shared with other application data, token cache entries compete for space and can be evicted prematurely.
Solution
Step 1: Check the eviction policy
Check the current eviction policy:
redis-cli CONFIG GET maxmemory-policy
For token caches, volatile-lru (the default) is appropriate because token cache entries have expirations. However, if other data without expirations consumes memory, token entries are evicted first.
Step 2: Use a dedicated Redis instance
Isolate the token cache from other application data by using a dedicated Redis instance:
{
"ConnectionStrings": {
"RedisTokenCache": "token-cache-redis.redis.cache.windows.net:6380,password=...,ssl=True,abortConnect=False",
"RedisAppData": "app-data-redis.redis.cache.windows.net:6380,password=...,ssl=True,abortConnect=False"
}
}
// Register the token cache Redis instance specifically for distributed caching.
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("RedisTokenCache");
});
Step 3: Increase the Redis memory limit
If a dedicated instance isn't feasible, increase the maxmemory setting. In Azure Cache for Redis, scale up to a higher tier or increase the cache size.
Step 4: Set appropriate cache entry expirations
Set reasonable expirations so stale entries are removed before memory runs out:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
options.SlidingExpiration = TimeSpan.FromHours(2);
});
SQL distributed cache table growth
Symptom
The SQL distributed cache table grows continuously, consuming disk space. Database queries against the cache table slow over time, and you might see warnings about table size or storage limits.
Cause
The SQL Server distributed cache (Microsoft.Extensions.Caching.SqlServer) doesn't automatically remove expired entries. Expired entries remain until explicitly purged, causing unbounded table growth, degraded query performance, and storage consumption.
Solution
Step 1: Set up a recurring cleanup job
Create a SQL Server Agent job or scheduled task to periodically remove expired entries:
-- Delete expired entries from the SQL distributed cache table.
-- Schedule this query to run every 30 minutes.
DELETE FROM [dbo].[TokenCache]
WHERE ExpiresAtTime < GETUTCDATE();
Tip
In Azure SQL Database, where SQL Server Agent isn't available, use Azure Automation, Azure Functions with a timer trigger, or Elastic Jobs to schedule the cleanup.
Step 2: Add an index for efficient cleanup
If the cache table doesn't already have an index on the expiration column, add one to speed up the delete operation:
CREATE NONCLUSTERED INDEX IX_TokenCache_ExpiresAtTime
ON [dbo].[TokenCache] (ExpiresAtTime);
Step 3: Monitor table size
Add monitoring to track the row count and table size over time:
SELECT
COUNT(*) AS TotalEntries,
COUNT(CASE WHEN ExpiresAtTime < GETUTCDATE() THEN 1 END) AS ExpiredEntries,
COUNT(CASE WHEN ExpiresAtTime >= GETUTCDATE() THEN 1 END) AS ActiveEntries
FROM [dbo].[TokenCache];
Step 4: Consider switching to Redis
If managing SQL cache cleanup is burdensome, switch to Redis, which handles expiration automatically through its built-in TTL mechanism:
// Replace SQL distributed cache with Redis.
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
});
General troubleshooting tips
Use these tips when your issue doesn't match a specific scenario in this article.
Verify the cache is being used
Add temporary logging to confirm tokens are read from and written to the cache:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.Encrypt = false; // Disable encryption temporarily for debugging only.
options.OnL2CacheFailure = (ex) =>
{
logger.LogError(ex, "L2 cache operation failed.");
return true;
};
});
Check for multiple cache registrations
If multiple calls to AddInMemoryTokenCaches() or AddDistributedTokenCaches() exist in your startup code, the last registration wins. Verify that only one cache type is registered.
Review the token lifetime
Access tokens have a finite lifetime (typically 60–90 minutes). If users report reauthenticating after this period, the behavior is expected rather than a cache problem. Refresh tokens obtain new access tokens silently and are stored in the cache. If the refresh token is missing or expired, the user must reauthenticate.
Test with a clean cache
When diagnosing issues, clear the cache to rule out corrupted or stale entries:
- In-memory cache: Restart the application.
- Redis: Run
FLUSHDBon the cache database. - SQL Server: Delete all rows from the cache table.
Token cache empty after application restart
Symptom
Users must reauthenticate after every application restart or redeployment. The distributed cache appears empty or tokens aren't persisted.
Cause
This issue typically occurs when you use an in-memory cache (AddInMemoryTokenCaches()) or the non-persistent distributed memory cache (AddDistributedMemoryCache()) in production. Neither option persists tokens across application restarts.
AddDistributedMemoryCache() registers an IDistributedCache implementation that stores data in memory. Despite the "distributed" name, it doesn't persist data externally and is intended only for development and testing.
Solution
Switch to a persistent distributed cache:
// Register a persistent cache (Redis example).
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp_";
});
// Use distributed token caches instead of in-memory.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
Warning
Don't confuse AddDistributedMemoryCache() with a persistent distributed cache. Use AddStackExchangeRedisCache() (Redis), AddDistributedSqlServerCache() (SQL Server), or another persistent IDistributedCache implementation for production workloads.