Web Development

Web Authentication Patterns: Sessions vs JWT Tokens

Learn which web authentication patterns actually fit your app. Compare server sessions vs JWT tokens on security, scalability, and token storage tradeoffs.

By Laxaar Engineering Team Aug 10, 2026 9 min read
Web Authentication Patterns: Sessions vs JWT Tokens

Authentication bugs are expensive. A misconfigured JWT library, an XSS-exposed token in localStorage, or a missing httpOnly flag can hand an attacker persistent access to every user account. The choice between server sessions and JWTs isn't just an architecture preference. It determines your attack surface, your logout semantics, and how much state management lands in your database versus your client.

Most tutorials present sessions and JWTs as interchangeable paths to the same goal. They're not. Each pattern carries a fundamentally different trust model, and conflating them produces systems that combine the downsides of both. Our take: the majority of web applications should default to server-managed sessions, and reach for JWTs only when the use case genuinely requires them.

We've seen this play out repeatedly at Laxaar across dozens of production systems. Teams adopt JWTs because they read that JWTs are "stateless and scalable," then spend weeks bolting on token revocation, refresh rotation, and secure storage logic that a cookie-based session would have handled for free.

What you'll learn

How server sessions work under the hood

A server session is a record stored on the server (in memory, Redis, a database, or a file system) that maps a random opaque ID to a user's identity and any session data. The browser receives that ID as a cookie, sends it on every subsequent request, and the server looks up the session record to authenticate the caller.

The session ID itself carries no information. It's a pointer. If an attacker steals it, you can invalidate the session record server-side and the stolen ID becomes worthless immediately. That's the key property: revocation is instant and guaranteed.

Browser                        Server                     Session Store
  |                               |                              |
  |-- POST /login (credentials) -->|                              |
  |                               |-- store session record ------>|
  |<-- Set-Cookie: sid=abc123 ---|                              |
  |                               |                              |
  |-- GET /dashboard (sid=abc123)->|                              |
  |                               |-- lookup sid=abc123 -------->|
  |                               |<-- { userId: 42, role: admin}|
  |<-- 200 OK (dashboard data) --|                              |

Sessions are stateful on the server side. That's often described as a drawback, but for a typical web application that already has a database, it's not. The session store is just another table or Redis key namespace.

How JWTs work and what stateless really means

A JSON Web Token is a self-contained, signed payload that encodes claims like a user ID, roles, and an expiry timestamp. The server signs it with a private key or a shared secret. Any service with the corresponding public key (or the same secret) can verify the signature without contacting a central store.

// Decoded JWT payload
{
  "sub": "user_42",
  "role": "admin",
  "iat": 1749470000,
  "exp": 1749473600
}

Stateless means the server holds no session record. Verification is a cryptographic operation, not a database read. This is genuinely valuable when multiple independent services need to trust the same token: a microservices mesh, a third-party API consumer, or a cross-domain scenario where setting cookies is impractical.

The tradeoff is that stateless tokens can't be revoked before they expire. If you issue a 30-minute access token and the user logs out, that token remains cryptographically valid for the remaining lifetime. Every "stateless JWT" system that needs real logout ends up maintaining a server-side revocation list. At that point it's no longer stateless, and you've added complexity without the simplicity win.

Security tradeoffs: storage, revocation, and XSS

Where you store a token on the client matters enormously.

localStorage is accessible to any JavaScript running on the page. A single XSS vulnerability (an injected script, a malicious npm dependency, a prototype pollution exploit) can silently exfiltrate every token in storage. Tokens stolen this way are valid until expiry, and the user has no way to know they've been compromised.

httpOnly cookies are not accessible to JavaScript at all. XSS cannot read them. CSRF is the relevant concern instead, and it's mitigated with the SameSite=Strict or SameSite=Lax attribute on modern browsers. Sessions delivered via httpOnly cookies have a substantially smaller XSS attack surface than JWTs stored in localStorage.

Refresh tokens deserve their own attention. A long-lived refresh token stored in localStorage is effectively a persistent credential. Rotating it on every use (refresh token rotation) limits the blast radius, but the rotation logic needs careful implementation. A race condition during token refresh can log out legitimate users or, worse, leave a stolen refresh token alive.

Scalability: when stateless actually helps

The "sessions don't scale" argument has a kernel of truth that's usually overstated. Sticky sessions, where all requests from a given user route to the same server instance, are genuinely a scaling constraint. But sticky sessions are an implementation choice, not a property of the session model. Move your session store to Redis and your application servers become stateless at the infrastructure level while still using server-side sessions at the application level.

JWT verification is a CPU operation, not a network round-trip. At very high read-heavy request volumes (millions of requests per second across hundreds of services), eliminating that Redis lookup per request does produce measurable savings. For the load profile of a typical SaaS, mobile backend, or internal tool, the difference is negligible.

Stateless tokens genuinely help when you have service-to-service authentication. A backend service calling another backend service using a short-lived signed token is cleaner than proxying session cookies through an API boundary. That's the architecture where JWTs earn their keep.

Refresh tokens and silent renewal

Access tokens are short-lived by design, typically 5 to 60 minutes. Refresh tokens are long-lived (hours, days, or weeks) and are used to obtain new access tokens without asking the user to re-authenticate.

The standard flow:

1. User logs in → server issues access_token (15min) + refresh_token (7d, httpOnly cookie)
2. Client uses access_token in Authorization header
3. access_token expires → client sends refresh_token to /auth/refresh
4. Server validates refresh_token, issues new access_token (and new refresh_token if rotating)
5. Client retries original request with new access_token

Refresh token rotation means the server invalidates the old refresh token each time a new one is issued. If a previously rotated token arrives at the server, that signals potential theft, and the entire token family should be revoked.

This is the minimum viable implementation for JWT-based auth in a user-facing app. Skipping rotation is a security gap. Implementing it correctly requires careful handling of concurrent refresh requests, which are a common source of bugs when multiple browser tabs race to refresh the same token.

Sessions vs JWT comparison table

PropertyServer SessionsJWT (Access + Refresh)
RevocationInstant — delete session recordRequires revocation list or wait for expiry
Storage (client)httpOnly cookieAccess token in memory; refresh in httpOnly cookie
XSS exposureLow (cookie not readable by JS)Higher if access token stored in localStorage
CSRF exposureRequires SameSite + CSRF tokenNot applicable for Authorization header usage
Cross-domain / API accessAwkward (cookies have origin restrictions)Natural fit (Authorization: Bearer)
Service-to-service authNot suitableGood fit
Implementation complexityLowModerate to high (rotation, silent renewal)
Database reads per requestOne session lookupZero (or one for revocation check)
Logout reliabilityImmediate and guaranteedBest-effort until expiry

When to use JWTs instead of sessions

Sessions should be the default for web applications where the client and server share the same origin (or a subdomain). Reserve JWTs for cases where the access patterns genuinely require them:

Cross-domain API access. A mobile app calling a REST API on a different domain can't rely on cookies with the same ease as a same-origin web app. A bearer token in the Authorization header is the natural fit.

Service-to-service authentication. Short-lived, signed tokens for internal API calls between backend services avoid the overhead of a shared session store and keep service coupling low.

Third-party integrations. If you're building an OAuth authorization server or issuing tokens that a partner's API will verify, JWTs are the standard that third-party systems expect.

Federated identity. When you delegate authentication to an identity provider (Auth0, Cognito, Okta), the provider issues JWTs and you verify them locally. You're consuming their authentication, not building your own.

Outside these scenarios, the most common reason teams choose JWTs is that they're more talked about. Not that they're more appropriate. A session-based system with Redis and httpOnly cookies handles the authentication needs of most SaaS applications with less code, fewer edge cases, and a more reliable logout experience.

If you're building a custom web application and aren't sure which pattern to reach for, start with sessions. Migrate to JWT-backed tokens only when a specific requirement forces the issue.

For teams building SaaS platforms or consumer apps with third-party integrations, the hybrid approach (sessions for user-facing web, JWTs for API and mobile clients) is often the most practical middle ground.

The Laxaar team has implemented both patterns across production systems and the pattern that causes the fewest incidents is the one that matches the trust model your application actually needs, not the one with the most conference talks about it.

Frequently Asked Questions

Should we store JWTs in localStorage or cookies?

Never store JWTs in localStorage if you can avoid it. Use httpOnly cookies for refresh tokens. JavaScript on the page can't read them, which eliminates the most common token-theft vector. For access tokens, keeping them in memory (a JavaScript variable, not localStorage) means they're lost on page refresh, which is an acceptable trade-off for better security. The workaround is silent refresh via the refresh token on page load.

How does logout work with JWTs?

Reliable logout with JWTs requires a server-side revocation list (a blocklist of token IDs, checked on each request) or very short token lifetimes combined with refresh token invalidation. Simply deleting the token from the client doesn't prevent a stolen copy from being used. If your application requires immediate logout (compliance, shared devices, account compromise scenarios), either use server sessions or maintain a revocation store that eliminates the "stateless" benefit.

What's the difference between access tokens and refresh tokens?

Access tokens are short-lived credentials (typically 5 to 60 minutes) sent with each API request. Refresh tokens are long-lived credentials (days to weeks) stored securely and used only to obtain new access tokens. The split exists to limit the window of exposure: if an access token is intercepted, it expires quickly. The refresh token stays in a secure httpOnly cookie and never travels in a request header.

Do sessions work for mobile apps?

Sessions work for mobile apps but require deliberate handling. You can persist the session cookie in the mobile app's secure cookie jar (both iOS and Android HTTP clients support this). The friction comes with cross-origin scenarios: if your mobile API lives on a different domain than your web app, cookie-based sessions need CORS configuration that permits credentials. Many mobile backend teams choose JWTs for this reason, accepting the implementation overhead in exchange for a simpler auth boundary.

Is JWT authentication more secure than sessions?

Neither is inherently more secure. The security depends on implementation choices. JWTs stored in localStorage with no rotation are less secure than a session ID in an httpOnly cookie. A session store with weak random ID generation is less secure than a properly signed JWT. The security comparison comes down to storage location, token lifetime, rotation policy, and how you handle revocation. Our view is that sessions are easier to implement securely by default; JWTs require more deliberate effort to get right.


Choosing the wrong authentication pattern doesn't break your app on day one. The debt surfaces later: during a security audit, a compliance review, or the first time a user reports their account was accessed after they logged out. If you're not sure which pattern fits your access patterns, talk to us about your project or explore our web development services. We've shipped both in production and we'll tell you which one actually makes sense for your situation.

Working on something like this?

Get a fixed scope, timeline, and price within one business day — no obligation.

Web AuthenticationSessions vs JWTToken Security
Grow your business with us

Take your business to the next level.

Tell us what you're building. We'll come back inside one business day with a fixed scope, timeline, and team — or an honest “this isn't a fit”.

ENGINEERING PHILOSOPHY

Code is useless if it's not comprehensible to those who maintain it. We write code the next person can actually understand.