Auth•Jun 2026•3 min read

Json Web Tokens vs Session Cookies

JWTs sell statelessness as a feature, but most teams pay for that flexibility with a revocation problem they never solve. Session cookies stay boring, server-controlled, and correct. For browser apps, the boring choice wins.

The short answer

Session Cookies over Json Web Tokens for most cases. For a browser-facing app, session cookies give you instant revocation, no token-in-localStorage XSS footgun, and a server that always knows the truth.

  • Pick Json Web Tokens if doing stateless service-to-service auth, third-party API access, or a distributed system where a shared session store is a real bottleneck — and you've actually built refresh + revocation, not just signed a token and called it done
  • Pick Session Cookies if building a normal web app with a normal backend. You want logout to work instantly, you don't want to hand-roll a token blocklist, and you'd rather your auth be boring
  • Also consider: PASETO if you want JWT's shape without the algorithm-confusion footguns; or opaque server-side tokens stored in an httpOnly cookie — the underrated middle path that gets you revocation AND a clean API surface.

— Nice Pick, opinionated tool recommendations

The core tradeoff

This is statelessness versus control, and people pick the wrong one constantly. A JWT carries its own claims, signed so the server can verify them without a database lookup. That's genuinely useful when a request crosses five services that shouldn't all hammer one session store. A session cookie is just an opaque ID pointing at server-side state — every request costs a lookup, but the server is the single source of truth. The dirty secret: most apps that reach for JWTs are a single backend talking to a single database. They paid for statelessness they will never spend, and inherited a revocation problem they will never properly solve. You don't get to use 'it's stateless' as a brag when your first feature request is 'log this user out everywhere right now' and you can't.

Revocation: the part nobody mentions in the blog post

A session cookie is killed by deleting one row. Logout is instant, 'ban this account' is instant, 'force re-auth after password change' is instant. A JWT is valid until it expires, full stop — that's the whole design. To revoke one, you build a blocklist your server checks on every request, which means you now do a database lookup on every request, which means you rebuilt sessions while keeping all of JWT's downsides. Short expiry plus refresh tokens is the real-world patch, and it's more moving parts than most teams budget for. If your threat model includes compromised accounts, stolen tokens, or any human pressing 'log out other devices,' stateless tokens fight you the entire way. Session cookies were quietly correct about this the whole time.

Where JWTs actually earn their keep

I'm not anti-JWT — I'm anti-cargo-cult. JWTs are the right call when verification must happen without a shared session store: an API gateway authorizing requests for downstream microservices, third-party clients you'll never run a cookie jar for, mobile and CLI clients where cookies are awkward, or federated auth where an identity provider mints a token your services trust independently. OIDC ID tokens are JWTs for good reason. The flexibility is real when the architecture is genuinely distributed. The failure mode is borrowing that architecture's auth pattern for an app that doesn't have that architecture. If you can name the specific service boundary the token crosses, use a JWT for it. If you can't name it, you don't need it — you're just adding a signing key and an expiry-tuning argument to your standups.

The security footguns

JWTs hand you sharper edges. Storing them in localStorage for SPA convenience exposes them to any XSS — and now an attacker has a portable, valid-until-expiry credential, not a cookie pinned to httpOnly. The 'alg: none' and RS256-downgraded-to-HS256 confusion attacks have burned real production systems because the spec lets the token tell you how to verify it. Session cookies aren't free either — CSRF is the classic, fixed by SameSite plus a token. But the cookie defaults (httpOnly, Secure, SameSite) are well-trodden and the browser does the hard part for you. With JWTs you're choosing algorithms, managing key rotation, and resisting the urge to put auth state somewhere JavaScript can read. More rope, more knots. For a browser app, fewer sharp edges beats theoretical flexibility every single time.

Quick Comparison

FactorJson Web TokensSession Cookies
Revocation / instant logoutHard — needs a blocklist or short expiry + refresh tokensTrivial — delete one server-side row
Stateless / cross-service authNative — self-verifying, no shared session storeRequires a shared/central session store
Browser XSS exposureHigh if stored in localStorage; portable stolen credentialLow with httpOnly cookie — JS can't read it
Implementation complexityAlgorithm choice, key rotation, refresh flow, expiry tuningBoring and well-trodden; SameSite handles CSRF
Mobile / third-party / CLI clientsClean — no cookie jar needed, easy to pass in a headerAwkward outside the browser

The Verdict

Use Json Web Tokens if: You're doing stateless service-to-service auth, third-party API access, or a distributed system where a shared session store is a real bottleneck — and you've actually built refresh + revocation, not just signed a token and called it done.

Use Session Cookies if: You're building a normal web app with a normal backend. You want logout to work instantly, you don't want to hand-roll a token blocklist, and you'd rather your auth be boring.

Consider: PASETO if you want JWT's shape without the algorithm-confusion footguns; or opaque server-side tokens stored in an httpOnly cookie — the underrated middle path that gets you revocation AND a clean API surface.

🧊
The Bottom Line
Session Cookies wins

For a browser-facing app, session cookies give you instant revocation, no token-in-localStorage XSS footgun, and a server that always knows the truth. JWTs win only when you genuinely need stateless, cross-service auth — and most teams reaching for them don't.

Related Comparisons

Disagree? nice@nicepick.dev