On this page
  1. What is the difference between sessions and JWTs?
  2. How do sessions and JWTs compare?
  3. Which should you choose?
  4. What is the safe way to use JWTs?
  5. Where AI coding assistants get this wrong
  6. Checklist
  7. FAQ
    1. What is the difference between a session cookie and a JWT?
    2. Are JWTs more secure than sessions?
    3. Should I store my JWT in localStorage or a cookie?
    4. When do I actually need JWTs?
    5. Is JWT stateless actually faster?
    6. What about opaque tokens as a middle ground?
  8. Related topics
  9. Sources
comparison

Session Cookies vs JWT: Which Should You Use?

Sessions and JWTs solve the same problem differently. Compare revocation, storage, scale, and attack surface, and get a direct recommendation.

Quick answer

  • A session cookie holds an opaque ID the server looks up in a session store. A JWT carries signed claims the server verifies without a lookup.
  • That one difference decides revocation: sessions are revocable instantly because the server holds the record; JWTs are not, until they expire.
  • For a web app talking to its own backend, use server-side sessions. They are simpler, revocable, and have a smaller attack surface.
  • Choose JWTs when a third party must verify identity without calling your server — or when you need stateless scale and accept the trade-offs.

What is the difference between sessions and JWTs?

A session cookie and a JSON Web Token both answer the same question: who is making this request? They differ in where the evidence of that identity lives.

With a session, your server creates a random ID, stores what that ID means (which user, which permissions) in a server-side store, and sends only the ID to the browser in a cookie. Every request arrives with the cookie, and the server looks it up. The cookie is meaningless on its own — it is a key to a record the server controls.

With a JWT, the server puts the claims (user ID, roles, expiry) inside a signed token and sends the whole token to the client. The server verifies the signature and trusts the contents, with no database lookup. The token is self-describing and stateless.

Everything that follows — revocation, storage, caching, attack surface, and how many moving parts you operate — is a consequence of that single architectural difference.

Where this bites vibecoders

The specific way this bites vibecoders is a default that skips the decision. An assistant asked to “add JWT authentication” produces a working implementation, because JWT is the most common thing to find in tutorials and training data. Nothing in the generated code notes that the same feature could have been fifteen lines of framework middleware. The result is a system that is harder to revoke, easier to leak, and nobody documented why it was chosen.

How do sessions and JWTs compare?

DimensionServer-side sessionJWT
What the client holdsOpaque random IDFull signed claims
Server-side storageRequiredNone
RevocationImmediate — delete the recordDifficult until expiry
“Log out everywhere”TrivialNeeds a blocklist, which reintroduces state
Token sizeSmall (~32 bytes)Large (often 1–2 KB+)
Reads on every requestOne store lookupSignature verification (CPU)
Works across servicesNeeds shared session storeWorks out of the box
Scales horizontallyNeeds shared/ sticky storeScales freely
Content readable by clientNothing meaningfulBase64-decodable, though signed
CSRF exposureYes — cookie is sent automaticallyNo, if sent via Authorization header
XSS exposureMitigated by HttpOnlySevere if stored in localStorage
ComplexityLow — usually built inHigher — you assemble the pieces

Read the two bottom rows together, because they are the ones that cause incidents. Storing a JWT in localStorage to avoid CSRF trades one vulnerability for a worse one, since any XSS becomes total session theft. Storing a session ID in an HttpOnly cookie is the more defensible default, and the CSRF it introduces is a solved problem with well-understood mitigations.

Which should you choose?

Use server-side sessions for a web app talking to its own backend. This is the right default for most of the applications this site is written for, and the reasons are not close.

Sessions are revocable in one operation, which matters the moment a user reports a compromised account or you discover a vulnerability. They are usually a single line of framework middleware rather than a subsystem you assemble. They keep tokens tiny, so every request carries less data. And with HttpOnly cookies they resist the XSS theft path that dominates real breaches of token-based apps.

Use JWTs when one of these is genuinely true:

  • A third party must verify identity without calling your server. This is the actual reason JWTs exist, and it is a good one.
  • You are building a mobile or desktop client talking to an API over a non-browser transport, where HttpOnly cookies do not apply.
  • You have many stateless services and a shared session store would be the bottleneck.
  • You have short-lived access tokens paired with refresh tokens, and have genuinely designed the revocation story.

Do not choose JWTs because they are newer, because a tutorial used them, or because the framework example did. “Stateless” is a real benefit, but you pay for it in revocation complexity, and most applications never hit the scale that made the trade-off worth it.

What is the safe way to use JWTs?

If you have decided JWTs are right, these are the non-negotiable parts. Most JWT failures are implementation mistakes rather than problems with the format.

Store the token in an HttpOnly cookie, not localStorage. Script cannot read an HttpOnly cookie. If you do this, add CSRF protection — see What Is CSRF (Cross-Site Request Forgery)?.

Keep access tokens short-lived. Minutes, not days. A short lifetime bounds the damage from a stolen token and makes revocation less critical.

Validate the algorithm, do not just verify the signature. The classic alg: none and algorithm-confusion attacks let an attacker forge a token if you trust the header. Pin the expected algorithm server-side.

Never put secrets in the payload. A JWT is signed, not encrypted — anyone holding it can base64-decode the contents. No passwords, no API keys, nothing you would not paste into a public issue.

Use a strong library. Hand-rolled JWT handling is where the subtle bugs live. The mistakes are catalogued in JWT Security: Common Mistakes That Get Tokens Stolen.

Plan revocation before you ship. “Log out everywhere” and “disable this user’s account now” both need a mechanism — a short expiry, a version counter checked against a store, or a blocklist. Decide which, in advance.

Where AI coding assistants get this wrong

  • Defaulting to JWT because it is the most common answer in training data, without asking whether the app needs statelessness at all.
  • Storing the JWT in localStorage, which makes any XSS a complete session compromise, and calling it “more secure than cookies.”
  • Accepting the alg header from the token, enabling none and algorithm-confusion forgery.
  • Putting user data — email, permissions, sometimes a user ID plus internal fields — in the payload, then forgetting it is readable by the client.
  • No revocation story at all, so logout only clears the browser and a stolen token stays valid until it expires.
  • Long-lived tokens. A 30-day access token is a 30-day window for an attacker.
  • Verifying the signature but not the claims — audience, issuer, and expiry are frequently skipped.
  • Hand-rolling the token construction instead of using a maintained library.

Checklist

  • Default to server-side sessions unless you have a concrete reason for JWTs.
  • If using JWTs, confirm the reason is third-party verification or true stateless scale.
  • Store tokens in an HttpOnly cookie rather than localStorage.
  • Add CSRF protection when authenticating with cookies — tokens in an Authorization header avoid it.
  • Keep access tokens short-lived and rotate refresh tokens.
  • Pin the expected signing algorithm server-side; never trust the token’s alg header.
  • Treat the JWT payload as public — put no secrets in it.
  • Use a maintained library, not hand-written token handling.
  • Decide how revocation works before launch, and test it.
  • Set the right cookie flags: HttpOnly, Secure, and SameSite — see What Are Security Headers.
  • Verify that logging out actually invalidates the credential everywhere, not just in one browser.

FAQ

A session cookie carries an opaque ID that the server looks up in a session store to discover who you are. A JWT carries the claims itself, signed so the server can verify them without a lookup. That single difference decides everything else: sessions are revocable because the server holds the record, and JWTs are stateless but expire on their own schedule.

Are JWTs more secure than sessions?

No. They have different risk profiles rather than a security ranking. Sessions move the burden to the server, which makes revocation trivial and theft harder to exploit durably. JWTs remove the server-side store but are harder to revoke, and the common implementation of storing them in localStorage exposes them to theft through XSS. Choose based on your revocation and topology needs.

In an HttpOnly cookie, if you are building a web app. localStorage is readable by any JavaScript running on the page, so a single XSS bug hands the token to an attacker. An HttpOnly cookie is not readable by scripts and can be sent automatically. Using cookies is why you then need CSRF protection.

When do I actually need JWTs?

When a third party needs to verify identity without calling your server — a mobile app, an internal service, or a partner integrating with your API. For a single web app talking to its own backend, a server-side session is simpler, revocable, and easier to get right, which is why it is the better default.

Is JWT stateless actually faster?

Only slightly, and only under load. Verifying a signature is cheap; the store lookup a session requires is also cheap. The real difference appears at horizontal scale, where sessions need a shared store or sticky routing and JWTs need nothing. If you are not scaling horizontally across many instances, statelessness buys you very little.

What about opaque tokens as a middle ground?

Worth knowing about. An opaque token is a random string the server looks up — a session by another name — but stored client-side like a JWT, which lets you keep the API stateless in shape while retaining easy revocation. It is a pragmatic choice when you want one code path across browser and mobile without accepting JWT’s revocation problems.

Sources

Share: