JWT vs Session Authentication: Which Should You Use?

"JWT or sessions?" is one of the most repeated authentication questions in web development, and the answer is more nuanced than the popular blog posts suggest. Both work; both can be implemented securely; both have failure modes that are easy to fall into. This guide walks through how each approach actually works, the security tradeoffs, and a concrete decision framework so you stop second-guessing the choice.

The Short Answer

For a typical web app where users log in and stay logged in across visits, sessions are the safer default. They are simple, well understood, supported by every major framework, and trivially revocable. Use JWTs when you need stateless, cross-service, or short-lived tokens — for example, OAuth access tokens, signed download URLs, password-reset links, or service-to-service authentication where you cannot share a session store.

The most expensive mistakes happen when teams reach for JWTs because they are "more modern" and end up reinventing session storage badly: storing tokens in localStorage, building a custom revocation list, never rotating signing keys, and accepting tokens long after a user has logged out. If you find yourself adding all of those, you have built a worse version of sessions. Pick the right tool.

How Session Authentication Works

Session-based auth has been the default since the 1990s. The flow:

  1. The user submits credentials. The server verifies them against the user database.
  2. The server creates a session record in a store (database, Redis, in-memory) — usually with a random session ID, the user ID, an expiry, and any session-scoped metadata.
  3. The server sends a Set-Cookie header with the session ID, marked HttpOnly, Secure, and SameSite=Strict or Lax.
  4. On every subsequent request, the browser sends the cookie. The server looks up the session record by ID and authenticates the request.
  5. To log out, the server deletes the session record. The cookie immediately becomes useless.
// Express + express-session example
app.post('/login', async (req, res) => {
  const user = await db.users.verify(req.body.email, req.body.password);
  if (!user) return res.status(401).send('Invalid credentials');
  req.session.userId = user.id;
  res.json({ ok: true });
});

app.get('/me', requireSession, (req, res) => {
  res.json({ userId: req.session.userId });
});

app.post('/logout', (req, res) => {
  req.session.destroy(() => res.json({ ok: true }));
});

The session ID itself is opaque — it carries no meaning. All the state lives on the server, which is why this approach is sometimes called "stateful" auth.

How JWT Authentication Works

A JSON Web Token packs claims (user ID, expiry, scopes) into a payload, signs the payload with a secret or a private key, and Base64-encodes the result. The full token is three Base64URL segments joined by dots:

header.payload.signature

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MDA...

The flow:

  1. User submits credentials. The server verifies them.
  2. The server constructs a JWT with claims like sub (subject — the user ID), exp (expiry), and iat (issued at), then signs it.
  3. The server returns the JWT — typically in a JSON response or a cookie.
  4. On every subsequent request, the client sends the JWT in the Authorization: Bearer header (or a cookie). The server verifies the signature and trusts the claims if the signature is valid and the token has not expired.

Critically, the server stores nothing about the token. Verification is just a signature check, which is why JWTs are called "stateless" auth.

Want to inspect a real JWT? Paste one into the JWT decoder to see the header, payload, and expiry — or read the JWT decoding guide for a full walkthrough of the structure.

The Real Tradeoff: State vs Statelessness

Every other comparison flows from this one. Sessions keep state on the server; JWTs encode state in the token. That choice has consequences:

Revocation

Sessions are trivially revocable: delete the row, the cookie no longer works. JWTs cannot be revoked without breaking statelessness — the server has no record of which tokens it has issued. In practice, JWT-based systems either accept that compromised tokens are valid until they expire (and use very short expiries with refresh tokens), or they bolt on a denylist of revoked jti claims, which re-introduces the server-side state JWTs were supposed to avoid.

Logout

"Log out everywhere" is a one-liner with sessions: DELETE FROM sessions WHERE user_id = ?. With JWTs, you typically increment a per-user version number stored in the database and embed it as a claim. On every request, the server compares the token's version to the database value — and now you have a per-request lookup again.

Token Size

A session cookie is a 32-byte random string. A JWT is hundreds of bytes — sometimes more than a kilobyte if it carries roles, permissions, and metadata. That overhead lands on every single request, which adds up on chatty APIs.

Horizontal Scaling

Sessions need a shared session store across server instances. Most teams reach for Redis. JWTs work without one — any server with the public key can verify a token. This is the genuine win for JWTs in microservice and serverless architectures, where sharing a session store can be operationally awkward.

Latency

Session validation is a database or cache lookup (usually a few milliseconds with Redis). JWT validation is a CPU-bound signature check (microseconds). For most apps the difference is invisible; for high-throughput APIs the JWT advantage is real.

Security Pitfalls

JWT Pitfalls

  • alg: none attacks. Some libraries historically accepted tokens with "alg": "none" in the header — meaning no signature. Always pin the expected algorithm in your verifier. Most modern libraries reject none by default, but legacy versions may not.
  • Algorithm confusion. If your verifier accepts both HMAC and RSA without checking, an attacker can sign a token with the public key as an HMAC secret. Always verify with the algorithm that matches the key type.
  • Weak HMAC secrets. Anything shorter than 32 bytes of cryptographic randomness is brute-forceable. Use a key generator, not a dictionary word.
  • Missing expiry. Always set exp, and always check it. A JWT without expiry is a bearer token that lives forever.
  • Storage in localStorage. Any XSS bug exfiltrates the token. Use HttpOnly cookies.

Session Pitfalls

  • CSRF. Because cookies are sent automatically, a malicious site can trigger requests on behalf of the user. Mitigate with SameSite=Strict (or Lax for top-level GET nav), and use CSRF tokens for state-changing endpoints if you need to support cross-origin contexts.
  • Session fixation. Always rotate the session ID after login. Most modern frameworks do this automatically.
  • Insecure session store. If your session store is unencrypted and readable by other processes, an attacker who reaches it can impersonate any user. Treat the store like a credential database.
  • Long-lived sessions. Sliding expiry is convenient but can leave a forgotten session active for months. Pair sliding expiry with an absolute maximum (e.g., 30 days) and require re-authentication for sensitive actions.

A Decision Framework

Use this checklist instead of choosing by vibes:

  • Single web app, traditional login flow, "stay logged in for weeks": Sessions. The maturity, revocation, and small payload all favour them.
  • Single-page app talking to your own backend: Sessions in HttpOnly cookies. There is no advantage to JWT for a same-origin SPA.
  • Mobile app talking to your own backend: Either works. Sessions if you can share a session store; JWTs (with refresh tokens) if you prefer stateless validation.
  • Public API consumed by third-party clients: JWTs (typically OAuth 2.0 access tokens). The third party cannot share your session store, so stateless tokens are the only practical option.
  • Microservices fanning out from a central auth service: JWTs for service-to-service calls, with the central auth service issuing short-lived tokens after verifying the user's session.
  • One-time-use links (password reset, email verification): JWTs are a clean fit. The token's expiry and claims are the entire mechanism.

The hybrid pattern — session cookie for the user-facing app plus a JWT issued for API calls — covers most modern stacks. There is no rule that says you must pick exactly one.

Refresh Tokens: How JWT Systems Get Sessions Back

When teams discover that pure JWT auth has no revocation, they typically add a refresh token: a long-lived secret stored server-side (usually in a database table) that can be exchanged for a new short-lived JWT. The refresh token is revocable; the JWT is not. Access tokens expire in 5 to 15 minutes, refresh tokens in days or weeks, and revocation works by deleting the refresh token row.

This pattern is fine, but notice the shape: the refresh token is essentially a session, and the JWT is a short-lived capability derived from it. You have re-invented sessions with extra steps. That is sometimes the right tradeoff — refresh tokens enable stateless services to verify access tokens without contacting the auth server — but if you do not need stateless verification, just use a session.

Inspect a JWT Now

If you want to see exactly what a JWT contains, paste one into our JWT decoder. The tool runs entirely in your browser, so even production tokens stay on your machine — useful for debugging missing or wrong claims without copying tokens into a third-party site.

Frequently Asked Questions

Is JWT more secure than session authentication?

No — neither is inherently more secure. Both can be implemented correctly or badly. Sessions have a maturity advantage because the patterns are decades old and most web frameworks ship secure defaults. JWTs require more decisions from the developer (algorithm choice, key rotation, expiry, refresh strategy) and have a long history of implementation bugs. The most common JWT vulnerabilities — accepting the alg: none header, weak HMAC keys, missing expiry checks, and no revocation — all stem from misuse rather than the standard itself.

Can you revoke a JWT before it expires?

Not without giving up statelessness. A pure JWT is valid until its exp claim, which is the entire point: the server checks the signature and trusts the token without a database lookup. To revoke early, you have to maintain a denylist of revoked token IDs (the jti claim) or check a per-user version number on every request — at which point you have re-introduced the database lookup that sessions were criticised for. Use short-lived access tokens (5 to 15 minutes) and a separate refresh token if revocation matters.

Should I store JWTs in localStorage or cookies?

Use HttpOnly, Secure, SameSite=Strict cookies. Storing JWTs in localStorage exposes them to any XSS payload that runs in your origin, because JavaScript can read the storage. HttpOnly cookies are inaccessible to scripts, which closes off the XSS exfiltration path entirely. The downside is that cookies are subject to CSRF, which you mitigate with SameSite=Strict (or Lax for top-level navigations) and CSRF tokens for state-changing requests. The cookie approach is the same defence used by session-based auth, which is why it has decades of battle-tested patterns behind it.

When are JWTs the right choice?

JWTs are well suited to short-lived, signed claims that need to cross trust boundaries — for example, an OAuth access token issued by an identity provider and accepted by multiple unrelated APIs. They are also useful for one-time signed URLs, password reset links, and email verification links, where the token lives long enough to be used once and then forgotten. They are a poor fit for long-lived web sessions where users expect to stay logged in for weeks and where a security incident requires immediate logout of every session.

Do session cookies work across mobile apps and microservices?

Yes, but with caveats. Mobile apps can store and send session cookies just like a browser — the platform HTTP clients handle this transparently. Across microservices, sessions require the services to share a session store (usually Redis or similar) or to validate the session via a central auth service. JWTs are sometimes preferred here because each service can validate the token independently with the public key, but you can achieve the same independence with signed session tokens or by issuing service-to-service JWTs alongside the user session cookie.