Every greenfield project in 2026 seems to start the same way: spin up an Express or Fastify server, install jsonwebtoken, store a signed token in a cookie or localStorage, and move on. JWTs have become the default session mechanism for modern web applications — and for many teams, that default is quietly creating problems they will not discover until production.
The recent resurgence of the “Stop Using JWTs” debate across Hacker News and the wider engineering community is not just contrarianism. It reflects a genuine architectural tension that development teams need to understand before choosing a session strategy.
TL;DR
- JWTs are an excellent tool for short-lived, cross-service authorisation tokens — but a poor default for browser session management
- Server-side sessions with opaque tokens remain simpler, more secure, and easier to revoke for most web applications
- The inability to instantly revoke a JWT without additional infrastructure (blocklists, short expiry + refresh tokens) negates much of the “stateless” benefit
- Microservice architectures genuinely benefit from JWTs for service-to-service authentication — the problem is using the same pattern for user-facing sessions
- A hybrid approach — server-side sessions for the browser, JWTs for internal service communication — gives most teams the best of both worlds
How We Got Here
JWTs solved a real problem when they gained popularity in the early 2010s. Single-page applications needed a way to authenticate API requests without relying on traditional cookie-based sessions. OAuth 2.0 and OpenID Connect standardised JWT as the token format, and the ecosystem followed.
The pitch was compelling: a self-contained, cryptographically signed token that carries user claims without requiring a database lookup on every request. Stateless. Scalable. Simple.
Except it is none of those things once you start handling the edge cases that every production application encounters.
The Revocation Problem Is Not a Minor Detail
The single biggest issue with JWTs for session management is revocation. When a user logs out, changes their password, or has their account compromised, you need to invalidate their session immediately. With server-side sessions, you delete the session record from your store — done. The next request fails authentication.
With JWTs, the token remains valid until it expires. A user who clicks “log out” might reasonably assume their session is terminated. In reality, anyone who captured that token can continue using it for the remainder of its lifetime.
The standard workaround is a token blocklist — a server-side store of revoked tokens checked on every request. But once you are maintaining server-side state to validate tokens, you have lost the primary architectural benefit of JWTs. You now have a more complex system that provides the same guarantees as a simple session store, with additional cryptographic overhead on every request.
Short-lived access tokens paired with refresh tokens are another common mitigation. This works, but it adds significant complexity: you need a refresh endpoint, token rotation logic, race condition handling for concurrent requests during refresh, and secure storage for the refresh token (which, ironically, often ends up being a server-side session).
The Token Size Tax
A typical session cookie is 32–64 bytes — an opaque identifier that maps to server-side state. A JWT carrying standard claims (subject, issuer, expiry, roles, permissions) easily reaches 800 bytes to 2 KB. Some teams embed extensive user profile data, pushing tokens beyond 4 KB.
This matters more than most developers realise. That token travels with every single HTTP request. On a page that makes 20 API calls on load, you are sending an additional 16–40 KB of redundant authentication data. For mobile users on constrained connections, this bandwidth tax is real. For APIs handling thousands of requests per second, the cumulative parsing and verification overhead adds up.
The localStorage Security Trap
Storing JWTs in localStorage remains disturbingly common despite being vulnerable to cross-site scripting (XSS) attacks. Any JavaScript running on your page — including compromised third-party scripts, browser extensions, or injected code — can read localStorage and exfiltrate tokens.
Server-side session cookies with HttpOnly, Secure, and SameSite=Strict flags are invisible to JavaScript entirely. They cannot be stolen via XSS. This is not a theoretical advantage — it is a fundamental security boundary that localStorage-based JWT storage simply cannot provide.
Yes, you can store JWTs in HttpOnly cookies. Many teams do. But at that point, you are using cookies for transport anyway, and you have added JWT parsing, verification, and size overhead on top of a cookie-based flow that would work perfectly well with an opaque session identifier.
Where JWTs Genuinely Excel
None of this means JWTs are bad technology. They are excellent for specific use cases:
Service-to-service authentication in microservice architectures. When Service A needs to call Service B on behalf of a user, a short-lived JWT containing the user’s identity and scopes lets Service B verify the request without calling back to an auth service. The token lives for seconds or minutes, revocation is less critical, and the stateless verification is a genuine architectural win.
One-time-use tokens. Email verification links, password reset tokens, and magic login links are ideal JWT use cases. The token is used once, has a short expiry, and does not need revocation infrastructure.
Federated identity and SSO. OpenID Connect’s use of JWTs as ID tokens makes perfect sense — these tokens are consumed once during the login flow to establish a session, not used as ongoing session credentials.
AI agent authentication. As agentic systems proliferate, JWTs work well for scoped, short-lived agent authorisation tokens where the agent needs to prove its identity and permissions to multiple services without maintaining persistent sessions.
The Hybrid Architecture Most Teams Actually Need
For the majority of web applications, the pragmatic architecture is straightforward:
Browser-facing layer: Use server-side sessions. Store an opaque session ID in an HttpOnly, Secure, SameSite cookie. Back it with Redis, PostgreSQL, or your database of choice. Instant revocation. No XSS exposure. Simple.
Service-to-service layer: Use short-lived JWTs (5–15 minute expiry). Your API gateway or BFF (backend-for-frontend) exchanges the session for an internal JWT when proxying requests to downstream services. The JWT carries the claims those services need without requiring each to call the auth service.
This gives you the security and simplicity of server-side sessions where it matters most — the browser — and the scalability of stateless JWTs where it matters most — internal service communication.
What About Scalability?
The “JWTs scale better because they’re stateless” argument was stronger a decade ago. Modern session stores are extraordinarily fast. Redis handles millions of operations per second with sub-millisecond latency. PostgreSQL with connection pooling handles session lookups effortlessly for most workloads.
If your application genuinely needs to avoid any server-side state for session management — and you have quantified this need, not assumed it — then JWTs with a robust refresh token flow and a blocklist for critical revocations can work. But most teams claiming to need stateless sessions are optimising for a scale problem they do not have while creating security and complexity problems they do.
A Practical Decision Framework
Before defaulting to JWTs for your next project, ask these questions:
- Do you need instant session revocation? If yes (and the answer is almost always yes), server-side sessions are simpler.
- Are you building a microservice architecture where services need to verify identity independently? Use JWTs for internal service-to-service calls.
- Is your “stateless” JWT flow actually stateless? If you have a blocklist, a refresh token store, or a token version check, you already have server-side state — just more complex state.
- Where are you storing the token in the browser? If
localStorage, reconsider immediately. IfHttpOnlycookies, ask whether the JWT overhead is earning its keep. - How large is your token? If it exceeds 1 KB, you are paying a meaningful bandwidth and parsing tax on every request.
The Bottom Line
JWTs are a powerful tool that has been misapplied as a general-purpose session mechanism. The industry’s collective realisation of this is not a rejection of JWTs — it is a maturation of how we think about session architecture.
For most web applications, server-side sessions remain the simpler, more secure, and more maintainable choice for browser-facing authentication. JWTs belong in the service layer, in one-time-use flows, and in federated identity — not as the default answer to “how do I handle user sessions?”
At REPTILEHAUS, we help development teams make these architectural decisions early — before they become expensive to reverse. Whether you are building a new SaaS platform, untangling an existing authentication flow, or designing a microservice architecture that needs to get auth right, our team has the experience to guide the decision. Get in touch if you would like to talk through your session architecture.

