JWT vs Session Cookies
Stateless and portable vs server-side and instantly revocable — choosing the right session mechanism.
For a broader overview of all API authentication options, see the API Authentication Methods guide.
- •You have multiple services that need to verify the same token
- •Stateless verification — no shared session store across servers
- •Tokens need to cross domains (mobile apps, SPAs with separate API)
- •Short-lived access with embedded claims (roles, tenant ID)
- •You need instant session invalidation (logout, ban, password change)
- •Traditional server-rendered web app (same domain)
- •You want automatic CSRF protection via SameSite cookies
- •Simpler infrastructure — no token signing keys to manage
Many apps use both: JWTs for API access tokens, session cookies for the browser session.
Core Comparison
| Aspect | JWT | Session Cookie |
|---|---|---|
| Storage | Client-side (memory, localStorage, or cookie) | Session ID in cookie; data on server |
| Verification | Cryptographic signature (no DB lookup) | Database/cache lookup on every request |
| Carries data? | Yes — claims in payload (sub, roles, exp…) | No — server resolves data from session store |
| Stateless? | Yes — each token is self-verifying | No — requires shared session store |
| Revocation | Hard — requires blocklist or short TTL | Instant — delete session from store |
| Expiry | Built-in exp claim | Sliding or absolute — controlled by server |
| CSRF risk | Low (if stored in memory, not cookie) | Mitigated by SameSite + CSRF tokens |
| XSS risk | High if stored in localStorage | Mitigated by HttpOnly flag |
| Cross-domain? | Yes — portable across origins | Same-site only (without CORS workarounds) |
| Scalability | High — no shared state | Requires sticky sessions or shared store (Redis) |
| Primary use case | APIs, microservices, mobile/SPA auth | Server-rendered web apps, monoliths |
JWTs Explained
A JWT (JSON Web Token) is a compact, signed token that encodes claims about the user directly in the token. The server issues it once, the client stores it, and subsequent requests present it. Any service that knows the signing key (or public key for RS256/ES256) can verify the token without querying a database.
The token consists of three Base64URL-encoded parts joined by dots: header.payload.signature. The header specifies the algorithm; the payload contains claims like sub (user ID), exp (expiry), and any custom claims (roles, tenant). The signature covers both header and payload.
The payload is Base64URL-encoded, not encrypted. Anyone who intercepts the token can read the claims. Never store sensitive data (passwords, SSNs, credit card numbers) in JWT claims. If confidentiality of claims is required, use JWE (JSON Web Encryption).
The JWT revocation problem
Because verification is stateless, a JWT remains valid until it expires — even if the user logs out or is banned. Common mitigations: short access token TTLs (15–60 min), refresh token rotation, or a small distributed blocklist in Redis. For scenarios where instant revocation is critical, session cookies are a simpler choice.
Session Cookies Explained
Session-based authentication stores the session state on the server (in memory, a database, or Redis) and gives the browser an opaque session ID in a cookie. On each request, the server looks up the session ID to find the associated user data. The cookie itself contains nothing useful — just a reference.
Because the session lives on the server, revocation is immediate: deleting the session record invalidates all tokens for that user at once. This makes sessions ideal for scenarios where you need to enforce logout, respond to password changes, or suspend accounts instantly.
The main trade-off is scalability. In a horizontally-scaled deployment, all servers must access the same session store. Sticky sessions (routing a user to the same server) work but limit flexibility. Sharing sessions via Redis is the standard production approach and adds an infrastructure dependency.
Cookie security flags
Always set HttpOnly (blocks JavaScript access, mitigates XSS), Secure (HTTPS only), and SameSite=Strict or Lax (CSRF mitigation). Without these flags, session cookies are vulnerable to common web attacks.
Real-World Patterns
React, Vue, or mobile apps calling a separate backend API are the classic JWT use case. The browser or app stores a short-lived access token (JWT) in memory and a refresh token in an HttpOnly cookie. API calls include the JWT in the Authorization: Bearer header.
Storing the access JWT in memory (not localStorage) limits XSS exposure to the current page session. The refresh token cookie is HttpOnly, so JavaScript can't read it. This is the recommended pattern for SPAs.
Next.js (pages router), Rails, Django, and similar frameworks typically use session cookies. The user logs in, the server creates a session, and a signed session ID cookie is sent to the browser. All subsequent requests automatically include the cookie — no JavaScript required to attach tokens.
This pattern is simpler, requires no token refresh logic, and the browser handles cookie management natively. SameSite cookies make CSRF protection automatic in modern browsers.
In microservices, the API gateway authenticates the incoming request (using sessions or OAuth), then issues a short-lived JWT for the internal request. Downstream services verify the JWT signature locally — no shared session store needed between services.
Users see sessions at the edge; services see JWTs internally. This hybrid gives you the easy revocation of sessions at the boundary and the stateless propagation of JWTs across services.
Many platforms use session cookies to authenticate browser-based web users (easy logout, no XSS risk) while issuing JWTs for third-party API consumers or mobile apps. The session is the authoritative state; JWTs are derived, short-lived tokens for specific integrations.
GitHub uses this pattern: browser sessions use cookies; API access uses tokens. The complexity is higher but you get the best properties of each approach for different client types.
Decision Checklist
- →Need instant logout or account suspension—Session Cookie
- →API consumed by mobile apps or cross-domain SPAs—JWT
- →Multiple backend services verify the same token—JWT
- →Traditional web app, same domain, server rendering—Session Cookie
- →High-scale API, no shared session store—JWT
- →Simplest possible auth implementation—Session Cookie
- →Embed user roles and tenant in every request—JWT
Security Considerations
Storing JWTs in localStorage
localStorage is accessible to any JavaScript on the page. An XSS vulnerability can steal all tokens. Store short-lived access JWTs in memory; store refresh tokens only in HttpOnly cookies.
JWTs without expiry
A JWT without an exp claim is valid forever. A stolen token can be used indefinitely. Always set short expiry for access tokens (15–60 minutes).
Session cookies without HttpOnly, Secure, SameSite
Missing HttpOnly allows JavaScript to read the session ID (XSS risk). Missing Secure allows transmission over HTTP. Missing SameSite exposes you to CSRF attacks. All three flags are required.
Sensitive data in JWT claims
JWT payloads are Base64URL-encoded, not encrypted. Anyone who obtains the token can decode and read all claims. Never store passwords, PII, or secrets in a JWT.
Accepting the "none" algorithm in JWTs
Some early JWT libraries accepted tokens with alg: none, requiring no signature. Always explicitly allowlist your accepted algorithms (e.g., HS256 or RS256 only).