Last updated: July 2026
Server-side sessions give you instant revocation, self-contained tokens like JWTs give you stateless scale, and cookies are the transport both usually ride on. For most distributed systems the right answer is a hybrid: short-lived tokens for API calls, a server-backed session for the login itself. That is exactly how Keycloak works, and since version 26.0 it persists every session to the database by default.

Quick comparison: cookies vs tokens vs server-side sessions
Before the table, one framing note that clears up most of the confusion in this debate: these three things are not actually competitors. A cookie is a transport (a header the browser attaches automatically). A token is a credential format (signed claims you can validate anywhere). A server-side session is a storage model (state lives on your infrastructure, the client only holds a pointer to it). You can put a JWT inside a cookie, and a session-ID cookie is useless without server-side storage behind it.
In practice, though, “cookie-based auth” usually means a session-ID cookie backed by server state, and “token-based auth” means a self-contained JWT in an Authorization header. Compared on those terms:
| Aspect | Session-ID cookie | Self-contained token (JWT) | Central session store |
|---|---|---|---|
| What the client holds | An opaque random ID | Signed claims, readable by anyone who has it | An opaque random ID |
| Per-request cost | One lookup against local session storage | Signature check only, no lookup | Lookup plus a network hop to the shared store |
| Revocation | Immediate: delete the server-side record | Hard: wait for expiry, or maintain a denylist | Immediate: delete the record from the store |
| Scaling | Sticky sessions, or move to a shared store | Any node validates independently | The store becomes a shared dependency to operate |
| Cross-domain | Bound to its domain | Works anywhere you can send a header | Needs shared infrastructure between services |
| Best fit | Server-rendered web apps | APIs, microservices, mobile clients | Regulated, revocation-sensitive systems |
Note what is not in that table: the claim that cookies “need a database lookup on every request” as a downside unique to them. Whether a lookup happens depends on what the cookie carries. A session-ID cookie implies one; a cookie carrying a signed token does not. The cookie itself costs a few hundred bytes of request header, nothing more.
How to choose
- Server-side sessions when revocation speed and audit trails matter more than raw scale: fintech, healthcare, admin panels.
- Tokens when services need to verify identity independently across regions and domains without phoning home.
- Session-ID cookies for classic server-rendered apps where one framework owns the whole request cycle.
- A hybrid for almost everything else, and we’ll look at Keycloak as a working example of one below.
How cookies handle session management
When a user logs in, the server responds with a Set-Cookie header. The browser stores the value and automatically attaches it to every subsequent request to that domain. That automatic attachment is both the whole appeal (zero client-side code) and the root of cookies’ most famous weakness, CSRF, because the browser attaches cookies to cross-site requests too unless you tell it not to.
The security attributes that matter in 2026
Modern cookie security is mostly about four things, all documented on MDN’s Set-Cookie reference:
HttpOnly: JavaScript can’t read the cookie, which takes session theft off the table for most XSS payloads.Secure: the cookie only travels over HTTPS.SameSite: controls cross-site sending.Strictnever sends cross-site,Laxsends only on top-level navigations,Nonesends everywhere but requiresSecure. If you don’t set it at all, Chrome treats the cookie asLax, with a two-minute grace window where fresh cookies are still sent on top-level cross-site POSTs.- The
__Host-prefix: naming a cookie__Host-sessionmakes the browser refuse it unless it hasSecure,Path=/, and noDomainattribute. That blocks subdomain cookie-injection attacks at the naming level.
A well-configured session cookie in 2026 looks like this:
Set-Cookie: __Host-session=8f3b1c...; Secure; HttpOnly; Path=/; SameSite=Lax
SameSite=Lax handles most CSRF cases for free, but it is not a complete replacement for CSRF tokens if your app deliberately accepts cross-site requests. And regardless of attributes, regenerate the session ID after login. The OWASP Session Management Cheat Sheet calls this out as the standard defense against session fixation.
The real size limits (smaller than you’ve heard)
A persistent myth says browsers allow “4 MB of cookies per domain”. They don’t, and never have. RFC 6265 sets the minimums browsers must support: at least 4096 bytes per cookie, at least 50 cookies per domain, and at least 3000 cookies total. Real budgets sit near those floors.
This matters for architecture. Every cookie rides along on every request to its domain, including requests for images and scripts. Stuff a serialized JWT with twenty claims into a cookie and you’re paying that tax on every asset fetch, and you’ll bump into per-cookie limits faster than you’d expect. Keep session cookies down to an ID or a compact token.
Are third-party cookies going away? No.
After years of deprecation countdowns, Chrome reversed course. Google’s October 2025 Privacy Sandbox update confirmed that third-party cookies are staying in Chrome, scrapped the planned standalone user prompt, and retired ten Privacy Sandbox APIs including Topics and Protected Audience. CHIPS, FedCM, and Private State Tokens survived the cut.
For authentication architects the takeaway is calm: first-party session cookies were never under threat, and the cross-site flows that worried identity teams (like silent SSO checks in iframes) no longer face a hard cliff in Chrome. Safari and Firefox still block third-party cookies, so cross-site session checks should still prefer purpose-built mechanisms like FedCM, but there’s no forced migration deadline anymore.
How token-based sessions work
Tokens flip the storage model: instead of the server remembering the session, the session data travels inside the credential. A JWT packs a header, a claims payload, and a signature into one string, and any service holding the public key can verify it without calling anyone. Clients typically send it as Authorization: Bearer <token>, which works across domains, services, and native apps where cookies get awkward. Paste one into our JWT Token Analyzer to see the structure for yourself.
Expiry is built in: the exp claim travels with the token, so every service enforces the same lifetime with no coordination. PASETO exists as a stricter alternative format that removes JWT’s algorithm-choice footguns, though JWT remains the ecosystem default.
Token risks, honestly
The stateless property cuts both ways. Three risks dominate:
- Leakage: a bearer token is a bearer token. Whoever holds it, is the user, until it expires. Tokens in
localStorageare readable by any XSS payload, which is why the standard advice is to keep them in memory or inHttpOnlycookies. - The revocation problem: you can’t un-sign a JWT. Logging a user out doesn’t invalidate tokens already issued. Short lifetimes plus refresh-token rotation is the usual answer, and our guide to JWT lifecycle management covers those patterns in depth.
- Algorithm confusion: older JWT libraries let attackers downgrade RS256 verification to HS256 and forge tokens with the public key. Pin your accepted algorithms server-side.
Notice the pattern: every serious mitigation for token weaknesses (denylists, refresh rotation, revocation endpoints) quietly reintroduces server-side state. Fully stateless auth is a spectrum you slide along, not a destination.
Sender-constrained tokens: forget “token binding”
Older articles (including a previous version of this one) recommended Token Binding to tie tokens to a specific client. Skip it: the spec is dead, browsers never shipped meaningful support, and Chrome removed its implementation years ago. The living options in 2026 are:
- DPoP (RFC 9449): the client holds a key pair and attaches a signed proof to each request, so a stolen access token is useless without the private key. This works for browser apps and mobile clients.
- mTLS certificate-bound tokens (RFC 8705): the token is bound to the client’s TLS certificate. Heavier to operate, common in banking and service-to-service traffic.
If a design doc in your org still says “token binding”, read it as “DPoP or mTLS” and update accordingly.
Why tokens scale so well
Each service validates signatures locally with a cached public key, so there’s no session store in the hot path, no shared infrastructure between regions, and no warm-up problem when you add nodes. For a globally distributed API surface, that independence is the entire pitch, and it’s a legitimate one. The cost is everything in the risks section above.
How server-side sessions work
The traditional model: on login, the server generates a random session ID, stores the real session state (identity, permissions, expiry) in a database, Redis, or an in-memory store, and hands the client only the ID, usually in a cookie. Every request triggers a lookup; every piece of sensitive state stays on infrastructure you control.
What OWASP says the numbers should be
The OWASP Session Management Cheat Sheet puts concrete figures on “secure session handling”, and they’re stricter than most defaults:
- Session IDs need at least 64 bits of entropy from a cryptographic random generator.
- Idle timeout: 2 to 5 minutes for high-value applications, 15 to 30 minutes for low-risk ones.
- Absolute timeout: typically 4 to 8 hours, regardless of activity.
- Regenerate the session ID after login or any privilege change.
If your framework’s session defaults are “until the browser closes”, you’re a long way from those numbers.
The scaling problem
Server-side sessions concentrate risk and load in one place. Every node needs access to the session store, so the store’s availability becomes your login availability. Geographic distribution hurts: either users far from the store eat latency on every request, or you replicate the store across regions and inherit consistency problems. Sticky-session load balancing dodges the shared store but means a dead node logs out everyone it was holding.
None of this is fatal. Redis clusters handle enormous session workloads. But it’s operational weight that token-based systems simply don’t carry, and it’s the honest reason stateless auth won the microservices era.
How Keycloak handles sessions: the hybrid in practice
Keycloak is worth studying here because it refuses to pick a side, and its architecture shows why the hybrid wins. If you’re new to it, start with our complete Keycloak guide. The short version: users authenticate once at the Keycloak server, which sets its own session cookie and tracks a server-side SSO session; applications then receive short-lived tokens. Server-side control where revocation matters, stateless tokens where scale matters. That combination is what powers real single sign-on across every client in a realm.
Sessions live in the database now, not just memory
This is the part most older articles get backwards. Before Keycloak 26, user sessions lived primarily in the Infinispan in-memory cache, and a restart could log everyone out. Since Keycloak 26.0, persistent user sessions are the default: every online user and client session is written to the database, which is the source of truth, while Infinispan acts as a cache in front of it. Sessions now survive restarts and upgrades out of the box. If you want the old memory-only behavior back (for throughput, at the cost of durability), you opt in explicitly with the volatile-user-sessions feature flag.
So the answer to “does Keycloak store sessions in memory or a database?” flipped in 2024, and any tuning advice that assumes memory-first sessions is stale.
The timeout defaults worth knowing
Session lifetimes live in the Admin Console under Realm settings > Sessions (token lifespans are on the neighboring Tokens tab). The defaults, per the Keycloak server administration docs:
| Setting | Default | What it controls |
|---|---|---|
| SSO Session Idle | 30 minutes | Logout after this much inactivity |
| SSO Session Max | 10 hours | Absolute session lifetime, active or not |
| Client Session Idle / Max | 0 (inherits SSO values) | Per-client refresh-token limits |
| Offline Session Idle | 30 days | Inactivity window for offline tokens |
| Offline Session Max | 60 days, if the “Limited” toggle is on | Absolute cap for offline sessions |
Two extras: Remember Me, when enabled, gets its own separate Idle and Max overrides so opted-in users can outlive the normal limits. And offline sessions, requested by a client with scope=offline_access, keep refresh working long after the SSO session dies, which is how mobile apps avoid monthly re-logins. We cover tuning all of these in our Keycloak session timeout configuration guide.
Worth noticing: Keycloak’s 30-minute idle default lands in OWASP’s “low-risk application” band. For an admin realm or anything high-value, tightening it is one of the cheapest security wins available.
Logout that actually reaches every app
Distributed logout is where server-side session tracking pays off. Keycloak gives you several layers of session management:
- Per-user and per-client session views in the Admin Console, with a sign-out-all button that ends every session for that user or client immediately.
- Push not-before (on the Revocation tab): sets a timestamp before which all previously issued tokens are rejected. It’s the emergency brake for a leaked signing key or a compromised client.
- OIDC Back-Channel Logout: when a session ends, Keycloak POSTs a signed logout token directly to each client’s configured backchannel logout URL, server to server. No browser involved, so it works even when the user’s tab is long closed. This is the mechanism that makes “log me out everywhere” trustworthy.
That last one deserves emphasis because it fixes the classic SSO complaint: front-channel logout via hidden iframes breaks silently when a browser blocks the frame or the user has left. Back-channel logout doesn’t care what the browser is doing.
Running this stack well (database-backed sessions, Infinispan clustering, logout plumbing) is real operational work, which is the part Skycloak’s managed Keycloak hosting takes off your plate.
Best practices, whichever model you pick
For cookies: __Host- prefix, Secure, HttpOnly, SameSite=Lax or stricter, and regenerate IDs on login. Treat any cookie readable by JavaScript as already stolen.
For tokens: short access-token lifetimes with refresh rotation, pinned algorithms, minimal claims (JWTs are encoded, not encrypted, so anything in the payload is public), and sender-constraining via DPoP where token theft is in your threat model.
For server-side sessions: meet the OWASP entropy and timeout numbers above, encrypt traffic to the session store, and test what happens to logins when the store has a bad day.
The most common mistakes we see in real deployments are less exotic: tokens that outlive logout because nobody wired up revocation, SameSite left unset and relying on Chrome’s default that other browsers don’t share identically, offline tokens granted to web apps that never needed them, and session stores that quietly accumulate expired records for years.
Frequently asked questions
Are cookies still used for authentication in 2026?
Yes, heavily, and the panic about their death was overblown. Chrome confirmed in its October 2025 Privacy Sandbox update that third-party cookies are staying, and first-party session cookies were never going anywhere. An HttpOnly, Secure, SameSite cookie remains the safest place to keep a browser session credential.
What is the difference between a session cookie and a JWT?
A session cookie carries an opaque random ID that points to state stored on the server, so the server does a lookup and can revoke instantly. A JWT carries the signed session data itself, so any service can validate it locally but revoking it early is hard. They also aren’t mutually exclusive: a JWT can be delivered inside a cookie.
How long should a login session last before timing out?
OWASP recommends an idle timeout of 2 to 5 minutes for high-value applications and 15 to 30 minutes for low-risk ones, with an absolute timeout of roughly 4 to 8 hours. Keycloak’s defaults (30-minute idle, 10-hour max) are looser than that: the 10-hour maximum sits beyond OWASP’s recommended range, so tighten both for sensitive realms.
Does Keycloak store sessions in memory or a database?
Since Keycloak 26.0, user sessions are persisted to the database by default, with the Infinispan cache sitting in front for speed. That means sessions survive restarts and upgrades. The old memory-first behavior is now an explicit opt-out via the volatile-user-sessions feature.
How does Keycloak log a user out of every application at once?
Because Keycloak tracks every session server-side, an admin (or the user, from the account console) can end all of them in one action. Keycloak then sends OIDC Back-Channel Logout tokens directly to each client’s backchannel logout URL, server to server, so applications are notified even with no browser open. For token-level emergencies, the push not-before policy invalidates everything issued before a chosen timestamp.