JWT vs Session Cookies: How They Differ and When to Use Each

A session cookie is a key to state kept on the server; a JWT carries signed state to the server. That one difference drives every trade-off.

Try it: JWT Decoder

The comparison is usually framed as JWT versus cookies, but those are not the same kind of thing. A cookie is a way of sending data with every request. A JWT is a data format. The real choice is between a server-side session, where the browser holds a random ID and the server looks up who it belongs to, and a self-contained token, where the client holds signed claims that the server can check without a lookup. Once that is clear, the questions about revocation, storage and CSRF fall into place.

How a server-side session works

After a successful login, the server creates a session record (user ID, roles, expiry) in memory, a database or a store such as Redis, and sends the browser an unguessable ID in a cookie. The browser attaches the cookie to every later request to that site, and the server loads the record for each one. The ID means nothing on its own; 32 random bytes encode to 43 characters.

Set-Cookie: __Host-session=p-sLYX7J3ziMKxq-dadVMuQJNSVBHM5hrw7MJPihGQ0; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400

The __Host- prefix makes browsers reject the cookie unless it is Secure, has Path=/ and sets no Domain, so a subdomain cannot overwrite it.

How a JWT works

A JSON Web Token (RFC 7519) is three Base64URL-encoded parts joined by dots: a header naming the algorithm, a payload of claims such as sub and exp, and a signature over the first two. The server checks the signature and the expiry with a key and trusts the claims without asking a database. It is usually sent in an Authorization: Bearer header.

The payload is only encoded, not encrypted. Anyone holding the token can read it, which you can see by pasting one into the JWT Decoder. Never put secrets or personal data in a JWT you would not show the user. Encrypted tokens exist (JWE, RFC 7516), but most JWTs in use are signed only. For why the parts use - and _ instead of + and /, see Base64 vs Base64URL.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MTIiLCJpYXQiOjE3NjAwMDAwMDAsImV4cCI6MTc2MDAwMDkwMH0.08BFm85KQIp34VSetcibqwBVLpHKR3IZhI_IPNeTif4

// Reading the payload needs no key at all (browser):
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(part), c => c.charCodeAt(0));
JSON.parse(new TextDecoder().decode(bytes));
// { sub: 'user_8412', iat: 1760000000, exp: 1760000900 }

The trade-offs side by side

Server-side sessionJWT
Client holdsA random, meaningless IDSigned, readable claims
Server needsA session store, shared by every serverOnly the key to verify signatures
Logging a user out everywhereDelete the session: immediateNot possible before exp without extra state
Role or permission changeApplies on the next requestApplies when a new token is issued
Typical transportCookie, sent automaticallyAuthorization header, or a cookie
Size per request43-character ID in our example152 to 540 characters in our examples

Revocation, expiry and refresh tokens

Statelessness is the main selling point of JWTs and also their main weakness. A valid token stays valid until it expires, even after the user logs out, changes their password or is banned. The usual answer is to keep access tokens short-lived, often minutes rather than hours, and pair them with a longer-lived refresh token that the client exchanges for new access tokens. The refresh token is checked against server-side state, so revoking it stops new access tokens from being issued.

The OAuth 2.0 Security Best Current Practice (RFC 9700) describes refresh token rotation: every refresh returns a new refresh token and invalidates the old one, so a stolen token that gets reused can be detected. If you need instant revocation of access tokens as well, you end up keeping a deny list of token IDs (jti) or a per-user token version, which is server-side state again. At that point, check whether a plain session would be simpler.

Where to store the token: cookies, localStorage and XSS

Anything in localStorage or sessionStorage can be read by every script running on your page. One cross-site scripting (XSS) bug or one compromised third-party script is enough to copy the token and use it from anywhere until it expires.

A cookie marked HttpOnly cannot be read by JavaScript at all, so XSS cannot steal it. XSS can still send requests from the victim's open page, and the browser will attach the cookie, so HttpOnly limits the damage rather than preventing it. Preventing XSS remains essential either way; HttpOnly cookies simply make a successful attack less useful to the attacker.

CSRF and SameSite

The cookie's convenience is also its risk: the browser attaches it automatically, including to requests triggered by another website. That is cross-site request forgery (CSRF). Tokens sent in an Authorization header are not exposed to classic CSRF, because the browser does not attach a bearer token by itself; your own code has to add it.

  • SameSite=Strict: the cookie is never sent on requests started from another site, including following a link to yours.
  • SameSite=Lax: sent when the user navigates to your site with a normal link (a top-level GET), but not on cross-site form posts, fetches or embedded resources. Chrome applies Lax to cookies that do not set the attribute; set it explicitly rather than rely on that.
  • SameSite=None: sent everywhere, and it must be combined with Secure (Chrome rejects it otherwise). Needed only for genuine cross-site use such as embedded widgets.

"Same site" means the same registrable domain, so app.example.com and forum.example.com count as the same site. SameSite does not protect you from a compromised subdomain. For state-changing requests, keep a CSRF token or check the Origin header as well.

Token size

A session ID stays tiny no matter how much you know about the user. A JWT grows with every claim and with the signature algorithm. The tokens we generated for this guide were 152 characters for HS256 with three claims, 241 characters after adding a name, email address and role, and 540 characters for the same claims signed with RS256 and a 2048-bit key. RFC 6265 only requires browsers to support cookies of at least 4,096 bytes, including name and attributes, so a JWT packed with permissions can outgrow a cookie. That size is also sent with every single request.

Mistakes that break JWT security

  • Trusting the alg header. Pin the algorithms your server accepts. Accepting none, or letting a token switch from RS256 to HS256 so that the public key is used as an HMAC secret, lets attackers forge tokens.
  • Decoding instead of verifying. Reading the payload proves nothing; verify the signature with a maintained library first.
  • Skipping claim checks. Validate exp, nbf, iss and aud, so a token issued for one service is not accepted by another.

RFC 8725, the JWT Best Current Practices, covers these and more.

When each one fits

SituationUsually the better fit
Server-rendered web app on one domainSession cookie
Single-page app served from the same site as its APISession cookie, or a backend-for-frontend that keeps tokens on the server
Native mobile appShort-lived access token plus refresh token in the platform's secure storage
Microservices verifying requests without calling an auth serviceJWT access tokens signed with an asymmetric key such as RS256 or ES256
Third-party access to your APIOAuth 2.0 access tokens, which are often JWTs

The hybrid: a JWT inside an HttpOnly cookie

Many apps put the JWT itself in an HttpOnly; Secure; SameSite cookie. Scripts cannot read it, and servers can still verify it without a session lookup. The trade-offs come along too: the cookie is sent automatically, so CSRF defences are needed again; the 4 KB cookie budget applies; and revocation is exactly as hard as with any other JWT. It is a sound design for a browser app backed by stateless APIs, as long as you treat it as a cookie for CSRF purposes and as a JWT for revocation purposes.

Frequently Asked Questions

Is JWT more secure than session cookies?

No, neither is inherently more secure. Sessions are easier to revoke and reveal nothing to the client. JWTs avoid a session lookup but are hard to revoke before they expire. Where the credential is stored and how CSRF and XSS are handled matter more than the format.

Should I store a JWT in localStorage?

It is simple but exposes the token to any script on the page, so a single XSS bug can leak it. An HttpOnly, Secure cookie with an explicit SameSite attribute keeps it out of reach of JavaScript, at the cost of needing CSRF protection.

Can you invalidate a JWT?

Not by itself. A signed token stays valid until its exp time. To cut access sooner, keep access tokens short-lived, revoke the refresh token, or check token IDs against a deny list, which adds server-side state.

Is the data in a JWT encrypted?

Normally not. The header and payload are Base64URL-encoded JSON that anyone can decode; only the signature prevents changes. Use JWE if the contents must be confidential, or keep sensitive data out of the token.
Try it: JWT Decoder