Everything works fine in development. You log in, you get a cookie, life is good. You deploy to production, and a portion of your users can't authenticate. No useful error message. The browser just silently fails or shows a generic "something went wrong" page. Your logs contain 431 Request Header Fields Too Large or requests that arrive with a valid session cookie that your app promptly rejects.
The culprit is almost always cookie size. This post walks through why auth cookies grow out of control, how to spot the symptoms, and three concrete solutions you can apply today.
Why Auth Cookies Get Chonky (Large)
The default ASP.NET Core cookie authentication handler serializes your entire ClaimsPrincipal into the cookie. Every claim attached to the principal gets written into that cookie. In a simple application, this is fine because a handful of claims fits comfortably in a cookie.
Problems start when any of these happen:
Too many claims. An application that maps every user attribute into claims can easily end up with dozens of entries: roles, permissions, department codes, feature flags, and tenant metadata. Each claim is a key-value pair, and they accumulate fast. Role-heavy systems are especially prone to this. A user with 30 role assignments can produce a cookie that exceeds browser limits before you've added any other claims.
Multiple authentication schemes. When you run multiple cookie schemes side-by-side (external provider cookies, application session cookies, multi-tenancy schemes), each one writes its own cookie. Even if no single cookie is oversized, the combined header payload on every request may push you past the 8 KB limit that many web servers and proxies enforce.
ID tokens stored in the session. If you're using OpenID Connect and storing the id_token in the cookie (which is the default behavior of the OIDC handler), you're serializing a base64-encoded JWT into the cookie on every request. An ID token with a large payload can add 1-2 KB on its own.
Group or permission claims from an external IdP. When a user authenticates via Microsoft Entra/Azure AD, Okta, or Google Workspace, and you have group membership claims enabled, the IdP can include dozens — sometimes hundreds — of group identifiers in the token. If all of those get mapped into your application cookie, you're in trouble.
Symptoms to Look For
Silent authentication failures are the most frustrating symptom. The browser sends a cookie, but the cookie is truncated at the browser level (most browsers enforce a 4 KB limit per cookie), so the server receives a partial, invalid payload. Deserialization fails silently, and the user is treated as unauthenticated.
HTTP 431 Request Header Fields Too Large appears when the total header payload crosses the server's limit. You'll see this more often with reverse proxies (nginx, Apache, IIS ARR) than with Kestrel directly, because proxies tend to enforce stricter header limits.
Intermittent logout. A user with a borderline-sized cookie might authenticate fine most of the time, but after accumulating additional claims through a role assignment or group membership change, their next login suddenly fails.
Load balancer or WAF blocking requests. Some web application firewalls have limits as low as 4 KB on individual header values. Users on those networks will appear to be perpetually logged out.
You can verify the cookie size is the issue by decoding the base64-encoded cookie value from the browser's DevTools (Application > Cookies) and checking its byte length. Anything over 3 KB is worth investigating.
Fix #1: Clearing Previous Cookies
When a server issues cookies, the expectation is that they'll be around for a while. The stickiness of cookies, combined with their imperceptibility, can lead to a cookie pile-up. When working with external authentication in Duende IdentityServer, a strong recommendation, we ask developers to first call SignOutAsync before invoking Challenge. The SignOutAsync call will ensure any existing (and irrelevant) cookies from the external provider are removed before a new one is created.
Fix #2: Don't Store Tokens In Cookies
When working with external OpenID Connect providers, the default behavior is to save tokens when issuing the cookie. Saving tokens enables you to access APIs or properly sign out with the id_token_hint. That said, external providers may not be necessary in every case. Storing unnecessary values may lead to unexpected behavior and errors, as tokens from external providers can be enormous and beyond your control. If you are using external authentication with Duende IdentityServer, you may want to disable token storage.
services.AddAuthentication()
.AddOpenIdConnect(options =>
{
options.SaveTokens = false;
});
Fix #3: Set MapInboundClaims to false
When using external authentication with Microsoft-based products such as ADFS or Entra ID, you may want to set MapInboundClaims to false when calling AddOpenIdConnect to prevent claims from the external provider from being mapped.
services.AddAuthentication()
.AddOpenIdConnect(options =>
{
options.MapInboundClaims = false;
});
Microsoft's namespace for external claims is typically prefixed with http://schemas.microsoft.com/identity/claims/, which is significantly larger than the claim names used by OpenID Connect and can take up unnecessary space. A remnant of the Windows Auth and XML-heavy 2000s, these verbose identifiers should almost never appear in a cookie.
Fix #4: Reduce Cookie Size with OnTicketReceived
When your application externalizes authentication with Duende IdentityServer using OpenID Connect, you can implement the OIDC handler's OnTicketReceived callback to reduce cookie size by removing unnecessary claims. The callback is invoked after the external authentication process is complete, but before the authentication cookie is stored in the browser.
You can use this callback to remove any claims that are not needed by your solution. Here's an example that removes unused-claim:
services.AddAuthentication()
.AddOpenIdConnect("oidc", options =>
{
// ...
// Remove claims that are not needed
options.Events = new OpenIdConnectEvents()
{
OnTicketReceived = context =>
{
var identities = context.Principal?.Identities ?? Enumerable.Empty<ClaimsIdentity>();
foreach (var identity in identities)
{
var removedClaim = identity.FindFirst("unused-claim");
identity.TryRemoveClaim(removedClaim);
}
return Task.CompletedTask;
}
};
});
Fix #5: Server-Side Sessions
The cleanest fix is to stop storing session data in the cookie entirely. Duende IdentityServer's server-side sessions feature moves the session payload to the server and replaces it with a small opaque session reference in the cookie. The cookie becomes a thin key that points to the server state rather than containing that state itself.
Enable it in your IdentityServer setup:
builder.Services.AddIdentityServer()
.AddServerSideSessions();
This requires a backing store. For the Entity Framework operational store, the session table is included automatically. For a custom store, implement IServerSideSessionStore.
On the client side, if you're building an ASP.NET Core application that is not Duende IdentityServer itself, you get a similar benefit from Duende BFF, which stores tokens server-side and sends only a simple session cookie to the browser. If you're just using ASP.NET Core cookie authentication directly, you can achieve the same result with session-backed ticket stores:
builder.Services.AddDistributedMemoryCache(); // Or Redis, SQL Server, etc.
// Custom ITicketStore implementation that persists the ticket in IDistributedCache
builder.Services.AddSingleton<ITicketStore, DistributedCacheTicketStore>();
builder.Services.AddOptions<CookieAuthenticationOptions>(
CookieAuthenticationDefaults.AuthenticationScheme)
.Configure<ITicketStore>((options, store) =>
options.SessionStore = store);
A minimal ITicketStore backed by IDistributedCache:
public class DistributedCacheTicketStore : ITicketStore
{
private const string KeyPrefix = "auth-ticket-";
private readonly IDistributedCache _cache;
public DistributedCacheTicketStore(IDistributedCache cache)
{
_cache = cache;
}
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var key = KeyPrefix + Guid.NewGuid().ToString("N");
await RenewAsync(key, ticket);
return key;
}
public async Task RenewAsync(string key, AuthenticationTicket ticket)
{
var options = new DistributedCacheEntryOptions();
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc.HasValue)
{
options.SetAbsoluteExpiration(expiresUtc.Value);
}
var ticketBytes = TicketSerializer.Default.Serialize(ticket);
await _cache.SetAsync(key, ticketBytes, options);
}
public async Task<AuthenticationTicket?> RetrieveAsync(string key)
{
var bytes = await _cache.GetAsync(key);
if (bytes is null) return null;
return TicketSerializer.Default.Deserialize(bytes);
}
public Task RemoveAsync(string key)
{
return _cache.RemoveAsync(key);
}
}
In production, swap AddDistributedMemoryCache for a shared cache (Redis, SQL Server) so the store works across multiple application instances. For developers using Redis, remember to enable persistence to avoid losing data prematurely and to size your Redis storage appropriately.
Fix #6: Chunked Cookies
If you want to keep the claims in the cookie while working within the browser's per-cookie size limits, ASP.NET Core's cookie authentication middleware supports chunking. Chunking splits a single logical cookie into multiple Set-Cookie headers, each within the browser limit, and reassembles them on the server side.
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "app.auth";
// Split cookies at 3500 bytes to stay safely under the 4 KB per-cookie limit
options.CookieManager = new ChunkingCookieManager
{
ChunkSize = 3500
};
});
ChunkingCookieManager is built into Microsoft.AspNetCore.Authentication.Cookies — no additional packages needed. It handles the split/reassemble logic transparently.
This is a good quick fix, but it doesn't solve the underlying problem. If your cookie payload grows past 12-16 KB, even chunking will struggle (browsers typically cap the number of cookies per domain). Use this as a stopgap while you address the root cause.
Fix #7: Claim Filtering in IProfileService
The most sustainable fix is to stop putting so many claims in tokens and cookies in the first place. If you're using Duende IdentityServer, implement IProfileService to control exactly which claims get included in tokens:
public class FilteredProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
private readonly UserManager<ApplicationUser> _userManager;
public FilteredProfileService(
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
UserManager<ApplicationUser> userManager)
{
_claimsFactory = claimsFactory;
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
if (user is null) return;
var principal = await _claimsFactory.CreateAsync(user);
// Only include claims that are explicitly requested by the client's scope
var requestedClaimTypes = context.RequestedClaimTypes;
var claims = principal.Claims
.Where(c => requestedClaimTypes.Contains(c.Type))
.ToList();
// Add a small set of always-present claims
claims.Add(new Claim("tenant_id", user.TenantId));
// Do NOT bulk-add role claims here - load them on demand in the API instead
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
context.IsActive = user?.IsEnabled ?? false;
}
}
Register it:
builder.Services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<FilteredProfileService>();
The key principle is to load data on demand in your APIs (via token introspection or a database lookup using the sub claim) rather than embedding everything in the token upfront. Roles and permissions are often better handled through an authorization service call during API request processing than by being baked into a JWT that must be small enough to fit in a browser cookie.
For the OIDC handler on the client side, you can also prevent the id_token from being saved in the cookie by mapping it to a different location:
builder.Services.AddAuthentication()
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://your-identity-server";
options.ClientId = "your-client";
options.ResponseType = "code";
// Don't store the raw id_token in the cookie
options.SaveTokens = false;
// If you need access tokens for API calls, use BFF or a server-side token store
});
Choosing the Right Fix
Use server-side sessions if you need to store large amounts of user state and you're already running Duende IdentityServer. It's the most complete solution and adds session management capabilities (you can revoke sessions, query active sessions, and enforce inactivity timeouts).
Use claim filtering if the problem is too many claims, and you can refactor how your application loads permissions. This is the correct architectural fix, and it benefits every part of your system — smaller tokens, faster validation, less network overhead.
Use chunked cookies as a tactical fix when you need to unblock users immediately and can't do a larger refactor right now. It's a valid interim solution.
Most production systems end up with a combination: filtered claims in the token, server-side sessions for the IdentityServer, and careful scope configuration on clients so they only request the claims they actually need.
The 4 KB browser cookie limit has been there since the early days of the web. It's not going anywhere. Building around it is just part of shipping reliable authentication at scale.
P.S.: Check out our deployment troubleshooting documentation for other common production issues and suggested solutions.
Thanks for stopping by!
We hope this post helped you on your identity and security journey. If you need a hand with implementation, our docs are always open. For everything else, come hang out with the team and other developers on GitHub.
If you want to get early access to new features and products while collaborating with experts in security and identity standards, join us in our Duende Product Insiders program. And if you prefer your tech content in video form, our YouTube channel is the place to be. Don't forget to like and subscribe!
Questions? Comments? Just want to say hi? Leave a comment below and let's start a conversation.