← Guides

Developer Guide

JWT vs Session Cookies

Stateless JWTs versus an opaque session id in a cookie: revocation, XSS versus httpOnly, token size, and CSRF. Inspect HS256 locally — this is not a login tutorial.

jwt vs session cookiesstateless jwt versus session idhttponly cookie versus localstoragejwt revocationcsrf versus xss auth

After login you still have to remember who the browser is. Two common designs: the server stores a session row and the browser holds an opaque id in a cookie, or the server signs a JWT and the browser holds that ticket. The first is stateful. The second is stateless until you add a denylist.

This page compares that choice — revocation, XSS versus httpOnly, size, CSRF — not how to issue claims at login. /tools/jwt-debugger decodes a token locally, verifies HS256 with a secret you type in the tab, and shows exp / iat / nbf as Unix, UTC, and local time.

Where the session actually lives

A session cookie carries a random id (session_id=f47ac10b-…). The server looks up user, roles, and expiry. Delete the row and the next request is anonymous, even if the cookie is still in the browser.

A JWT carries the claims in the payload. Any service with the HMAC secret (HS256) or the issuer’s public key can verify it without a session table. The ticket stays valid until exp unless every replica consults something you can delete.

Revocation

Logging out of a session store is a delete. Logging out of a pure JWT is waiting, rotating keys, or checking a jti denylist — a small database again.

Short exp limits the window. Refresh tokens you can revoke restore a kill switch. Paste a token into /tools/jwt-debugger and read exp before you assume logout did anything. Expired and not-yet-valid badges appear after mount. Only HS256 is verified against the local secret; other algorithms are decoded, not verified.

XSS, localStorage, and httpOnly

localStorage is visible to JavaScript. XSS can send a JWT off-site. An httpOnly Secure cookie cannot be read by scripts. XSS can still ride the session (the browser sends the cookie), but it cannot copy the cookie value.

A JWT in localStorage is usually worse than an httpOnly session cookie. A JWT in an httpOnly cookie keeps the token off the JS heap and takes on cookie behavior, including CSRF.

CSRF and size

Cookies attach automatically. That is CSRF: another site can trigger a state-changing request unless you use SameSite and/or anti-CSRF tokens. An Authorization: Bearer header is not sent by a foreign <form>. Session cookies need CSRF defenses. Bearer JWTs need XSS defenses if JavaScript can read them.

A session id is a few dozen bytes. A JWT is header + payload + signature, often hundreds of bytes on every request. Keep fat claims out of the cookie.

When each design fits

Session id cookies fit server-rendered apps and admin tools where “revoke now” matters. JWTs fit APIs that must verify without sharing a session table — mobile clients, other backends, gateways — if exp is short and refresh tokens can be revoked.

You can put a JWT in a cookie or a session id in a header. The split is stateful lookup versus signed claims, not the word cookie versus token.

Common pitfalls

Calling a JWT more secure because it is signed — the payload is readable, and a stolen ticket works until exp. Storing it in localStorage, then later moving it into a cookie without CSRF protection.

Comparing exp to Date.now() without dividing by 1000. Pasting a production HMAC secret into a screen share of /tools/jwt-debugger. The secret stays in the tab, not on a server, and still belongs only in that tab.

  • /tools/jwt-debugger verifies HS256 only, with a local secret.
  • exp, iat, and nbf are Unix seconds; the debugger shows UTC and local.
  • Opaque session ids revoke by delete; signed JWTs do not, unless you planned for it.

Worked examples

Opaque session cookie

Set-Cookie: session_id=f47ac10b-58cc-4372-a567-0e02b2c3d479; HttpOnly; Secure; SameSite=Lax; Path=/

The server looks up that UUID. Deleting the row revokes the session. The cookie value is not a set of claims.

What a JWT debugger actually shows

Input
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Output
{
  "alg": "HS256",
  "typ": "JWT"
}

Paste a full token into /tools/jwt-debugger for payload times (iat 1700000000 is 2023-11-14 22:13:20 UTC) and local HS256 verify. RS256 is decode-only.

Frequently asked questions

Is a JWT more secure than a session cookie?

Not by default. A JWT in localStorage is usually worse than an httpOnly session cookie. Security follows storage, HTTPS, exp, and how you revoke.

How do I revoke a JWT?

You cannot unsign it. Shorten exp, revoke refresh tokens, rotate keys, or check a jti denylist. A session row is a delete.

Does an httpOnly JWT cookie still need CSRF protection?

Yes. The browser will attach the cookie. SameSite and an anti-CSRF token are the usual pair. Bearer headers do not get that automatic attach.

Why will the debugger not verify my RS256 token?

/tools/jwt-debugger only verifies HS256 with a secret you type in the page. It still decodes header and payload for other algorithms.

Does thedevtools.in store my token?

No. Decode and HS256 verify run in the browser. Do not screenshot production tokens.

Summary

Use an opaque httpOnly session cookie when you need instant revocation and a small header. Use a JWT when services must verify without that lookup — short exp, prefer httpOnly over localStorage, and plan CSRF if the JWT is a cookie. Inspect times and HS256 on /tools/jwt-debugger.

Try it yourself

Put what you've learned into practice with our free browser-native tool.

Open Tool →