14 categories See all →
home / blog / Developer

JWT vs Session-Based Authentication — Which Should You Use?

A balanced comparison of stateful sessions and stateless JWTs — scalability, revocation, storage, and the security tradeoffs that actually matter.

This question comes up in nearly every backend architecture discussion, and it usually gets answered badly — with someone declaring JWT "modern" and sessions "legacy," as if one has simply replaced the other. That's not true. Both are still widely used in production systems, including systems built by companies with excellent engineering teams, and the right choice genuinely depends on what you're building.

The core mechanical difference

Session-based auth is stateful. When a user logs in, the server creates a session record (usually in memory, Redis, or a database) and gives the browser a session ID — a short, opaque, random string — typically stored in a cookie. On every request, the server looks up that ID against its session store to figure out who's making the request and what they're allowed to do.

JWT auth is stateless. When a user logs in, the server creates a signed token containing the user's identity and whatever claims it needs, and hands the whole thing to the client. On every request, the server verifies the token's signature and reads the claims directly — no database lookup, no session store, no shared state required.

That one difference — does the server need to remember anything, or not — is the root of almost every other tradeoff below.

Scalability

This is JWT's clearest advantage. Because there's no server-side session store to consult, any server instance can verify a request independently. Add more servers behind a load balancer, and none of them need to share session state. With traditional session auth, you either need sticky sessions (routing a user's requests to the same server every time, which complicates load balancing and failover) or a centralized session store like Redis that every server queries — which is itself a piece of infrastructure you now have to run, scale, and keep available.

For a small-to-medium app with a handful of servers, a shared Redis session store is a completely reasonable, well-understood solution, and the operational cost is low. For something at genuinely large scale with many independent services, JWT's statelessness removes a dependency that would otherwise become a bottleneck.

Revocation — this is where JWT's story gets uncomfortable

With sessions, revoking access is trivial: delete the session record from the store. The user's next request fails immediately, because the server no longer recognizes the session ID. Logout, password change, admin-forced logout — all one delete operation.

With JWT, there is no built-in way to revoke a token before it expires. The server verifies tokens purely by checking the signature — it never "asks" anywhere whether the token is still valid, because asking anywhere would reintroduce the state you were trying to avoid. If someone steals a JWT with a 24-hour expiry, it remains valid for up to 24 hours no matter what you do on the server, unless you've built extra infrastructure specifically to handle this — typically a blocklist of revoked token IDs that you check on every request. But at that point, you've reintroduced a server-side lookup on every request, which gives up a meaningful chunk of JWT's statelessness advantage.

The common mitigation is short-lived access tokens (5–15 minutes) paired with longer-lived refresh tokens that are stored server-side and checked against a revocation list. This gets you most of JWT's scalability benefit while bounding how long a compromised token stays dangerous — but it's meaningfully more complex to implement correctly than either pure approach.

Payload size

A session ID is small — typically 32–64 characters, sent as a cookie on every request. A JWT carrying even a modest set of claims (user ID, roles, email, issued/expiry timestamps) is routinely several hundred bytes to a couple of kilobytes, and it also gets sent on every request. For high-traffic APIs or mobile clients on constrained networks, this difference in per-request overhead is real, even if it's rarely the deciding factor.

Where to store each one, and why it matters for security

This is the part people get wrong most often. Session IDs are conventionally stored in an HttpOnly, Secure cookie — a cookie flag that makes it inaccessible to JavaScript entirely. This protects it from being stolen via XSS (cross-site scripting): even if an attacker injects a malicious script into your page, that script cannot read an HttpOnly cookie. The tradeoff is that cookies are automatically attached to every request to the domain, which opens the door to CSRF (cross-site request forgery) — a malicious site tricking a logged-in user's browser into making a request your site then treats as authenticated. CSRF is a well-understood problem with well-understood defenses (CSRF tokens, SameSite cookie attributes).

JWTs are frequently stored in localStorage so client-side JavaScript can read them and attach them to API requests manually via an Authorization header. This sidesteps CSRF entirely, since the token isn't automatically sent by the browser. But it reopens XSS as a real threat — any script that runs on your page, including one injected through a vulnerable dependency or an unsanitized user input field, can read everything in localStorage and exfiltrate the token.

You can store a JWT in an HttpOnly cookie too, which gets you XSS protection back — but then you're back to handling CSRF, and you've also partly given up the "no cookie, pure API token" simplicity that made people reach for JWT in the first place. There's no storage location that's safe from both attack classes by default; it's a genuine tradeoff, not a solved problem.

So which should you actually use?

Building a traditional web app with a single backend, server-rendered or with a tightly coupled frontend, where users log in and out through your own UI? Session auth is simple, revocation is trivial, and the "stateless" advantage of JWT buys you very little because you don't have the distributed-services problem it was designed to solve.

Building an API consumed by multiple independent services, mobile apps, or third parties — where you genuinely need stateless verification across services that don't share infrastructure? JWT (often with short expiries and a refresh-token flow) is the better fit, and the added complexity is solving a real problem you actually have.

If you're building an internal admin tool or a small SaaS product with one backend, don't reach for JWT just because it sounds more modern — sessions will likely serve you better and be considerably simpler to reason about, especially around logout and revocation. You can inspect any JWT your own backend issues (to sanity-check the claims you're including) using our JWT Decoder.