JWT vs Session Authentication: Which Should You Use?
Sessions give you instant logout. JWTs skip the database lookup. A spec-backed comparison of JWTs, sessions and cookies, with RS256 vs HS256 and a decision flowchart.

11 min read · Last updated: October 25, 2026
In the JWT vs session authentication choice, use server sessions in an HttpOnly cookie for a browser app talking to its own backend, and use short-lived JWTs as bearer tokens for APIs called by other services or third-party clients. Sessions give you instant logout. JWTs give you verification without a database lookup. Most real systems use both.
Came here from Instagram, LinkedIn, X or YouTube? Here is the full source list you came for. Everything below is built from these, nothing else.
The specs
- RFC 7519, JSON Web Token (JWT), May 2015
- RFC 7518, JSON Web Algorithms (HS256, RS256), May 2015
- RFC 8725, JSON Web Token Best Current Practices, 2020
- RFC 6750, OAuth 2.0 Bearer Token Usage, October 2012
- RFC 6265, HTTP State Management Mechanism (cookies), April 2011
The original paper
The security guides
- OWASP Session Management Cheat Sheet
- OWASP JSON Web Token Cheat Sheet
- OWASP HTML5 Security Cheat Sheet (local storage)
The docs
Key takeaways
- A session stores state on the server and gives the browser a random ID. A JWT stores the claims inside the token, and the server only checks the signature.
- JWT and cookie are not rivals. JWT is a format and a cookie is a transport. You can put a JWT in a cookie.
- OWASP says session IDs need at least 64 bits of entropy, and an HMAC secret for JWTs needs at least 160 bits.
- RFC 6750 says bearer tokens should live one hour or less, because anyone holding one can use it.
- HS256 is the only Required JWT algorithm in RFC 7518. RS256 is Recommended and needs a key of 2048 bits or larger.
- A JWT is only stateless until you need logout. The moment you add a denylist, you are back to a server lookup.
JWT vs session authentication: what is the difference?
Session authentication keeps the logged-in user's state on the server and hands the browser an opaque, random session ID in a cookie. JWT authentication puts the user's claims (who they are, when the token expires) into a signed token, so any server holding the key can verify it without looking anything up.
That one difference drives every row in this table.
| Server session | JWT (stateless token) | |
|---|---|---|
| Where the state lives | Server (database, Redis, memory) | Inside the token, on the client |
| What the client holds | Random ID, meaningless on its own | Signed claims: sub, exp, aud and so on |
| Lookup on each request | Yes, find the session by ID | No, verify the signature |
| Logout and revocation | Delete the row. Instant | Wait for exp, or keep a denylist |
| Readable by the client | No | Yes. OWASP: signed JWTs are readable by anyone who obtains them |
| Size on the wire | A short ID | Grows with every claim you add |
| Works across services and domains | Hard, every service needs the session store | Easy, every service needs only the key |
| Typical transport | Cookie | Authorization: Bearer header, or a cookie |
| Best fit | Browser app with its own backend | APIs, microservices, mobile and third-party clients |
If the token format itself is new to you, read what a JWT is and why you would use one first, then come back.
How does cookie-based authentication work?
Cookie-based authentication works in four steps. You log in, the server creates a session and replies with a Set-Cookie header holding the session ID, the browser stores it, and the browser sends it back in a Cookie header on every later request to that site. The server reads the ID, finds the session, and knows who you are.
This is the loop RFC 6265 describes: "To store state, the origin server includes a Set-Cookie header in an HTTP response", and on later requests "the user agent returns a Cookie request header to the origin server."
- The user posts their email and password to
/login. - The server checks them, creates a session record, and generates a new random session ID.
- The server responds with a cookie:
HTTP/1.1 200 OK
Set-Cookie: __Host-sid=8f3a1c9e4b7d2a60; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800
- Every later request carries it automatically:
GET /api/me HTTP/1.1
Host: app.example.com
Cookie: __Host-sid=8f3a1c9e4b7d2a60
Each attribute does a specific job, according to MDN's Set-Cookie reference:
- HttpOnly forbids JavaScript from reading the cookie through
document.cookie, which blunts cross-site scripting (XSS) theft. - Secure sends the cookie only over
https:. - SameSite=Lax (or
Strict) keeps the cookie off most cross-site requests. OWASP calls this defense in depth against cross-site request forgery (CSRF), not a replacement for CSRF tokens. - The
__Host-prefix forcesSecure,Path=/and noDomain, so a subdomain cannot overwrite it. - Max-Age is the lifetime in seconds.
1800is 30 minutes.
OWASP's Session Management Cheat Sheet adds three rules worth memorising. Session IDs must have at least 64 bits of entropy from a cryptographically secure random generator. The ID must be regenerated after any privilege change, login included, to stop session fixation. And idle timeouts commonly run 2 to 5 minutes for high-value apps and 15 to 30 minutes for low-risk ones, enforced on the server.
How does JWT authentication work?
With JWT authentication, the server signs a token containing claims and gives it to the client. The client sends it back, usually in an Authorization: Bearer header. Each server verifies the signature, algorithm, expiry, issuer and audience. If all of them pass, it trusts the claims without a database call.
RFC 7519 defines a JWT as "a compact, URL-safe means of representing claims to be transferred between two parties." It registers seven standard claims: iss (issuer), sub (subject), aud (audience), exp (expiry), nbf (not before), iat (issued at) and jti (a unique ID).
Here is verification done properly in Node.js with the jose library:
import { jwtVerify } from "jose";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
export async function getUser(req) {
const token = req.headers.authorization?.replace(/^Bearer /, "");
const { payload } = await jwtVerify(token, secret, {
algorithms: ["HS256"],
issuer: "https://auth.example.com",
audience: "api.example.com",
});
return payload.sub;
}
The algorithms allowlist is not decoration. RFC 8725, the JWT Best Current Practices, says libraries "MUST enable the caller to specify a supported set of algorithms", and OWASP's JWT cheat sheet tells you to make sure "alg":"none" is never accepted. Checking aud matters too: RFC 8725 requires an audience claim whenever one issuer serves several relying parties.
Where do server sessions win?
Sessions win wherever you need control after login. Because the truth lives on your server, you can log a user out everywhere, kill a stolen session, or change their role, and it takes effect on the very next request. The client holds nothing but a random ID, so there is nothing to decode or tamper with.
- Instant revocation. Delete the session row and the user is out.
- Nothing leaks from the token. An opaque ID carries no data. A JWT payload is readable by anyone holding it, and OWASP notes claims can end up "in application logs, in browser storage, in referrer headers."
- Small cookies. RFC 6265 only asks browsers to support 4,096 bytes per cookie, name and attributes included. A JWT stuffed with roles and permissions can hit that. A session ID never will.
- Out of JavaScript's reach. OWASP's HTML5 Security Cheat Sheet is blunt about the alternative: "Do not store session identifiers in local storage as the data is always accessible by JavaScript."
The cost is the lookup. Every request needs the session store, and every service that authenticates users needs access to it.
Where do JWTs win?
JWTs win when the party verifying the user is not the party that logged them in. An API gateway, a second microservice, or a partner's server can verify a signed token with just a key, and with RS256 just a public key. No shared database and no call to an auth server on every request.
- No per-request lookup. Verification is local CPU work.
- Works across domains and services. A cookie is tied to a site. A bearer token goes wherever you send it.
- Fits non-browser clients. Mobile apps, CLIs and server-to-server calls handle an
Authorizationheader more naturally than a cookie jar. - Shared vocabulary.
iss,audandexptell every service who issued the token, who it is for, and whether it is still valid.
The cost is revocation. OWASP's JWT cheat sheet suggests short expiration times, or a denylist keyed on jti and iss, and admits that a denylist "makes sessions less stateless."
JWT vs cookies: what is the actual difference?
JWT vs cookies is a category mix-up. A cookie is a way to store and send data from the browser. A JWT is a data format for signed claims. The real choice is where your credential lives (a cookie or JavaScript storage) and what it contains (an opaque session ID or a JWT).
That gives you four combinations, and they are not equal:
| What it holds | Where it lives | Verdict |
|---|---|---|
| Session ID | HttpOnly cookie | The safe default for browser apps |
| JWT | HttpOnly cookie | Fine. RFC 6750 requires CSRF protection when tokens travel in cookies |
| JWT | localStorage | Avoid. OWASP: "A single Cross Site Scripting can be used to steal all the data in these objects" |
| JWT | Memory, sent as Bearer | Good for APIs and native clients. Lost on page reload in a browser |
The "keep your JWT in localStorage" pattern from older single-page app tutorials trades CSRF risk for XSS risk, and XSS is the one that steals the token outright.
JWT vs bearer token: are they the same thing?
No. A bearer token is a usage rule, and a JWT is one possible format. RFC 6750 defines a bearer token as one that any party in possession of it can use, with no proof of a cryptographic key. A bearer token can be a JWT, or a random opaque string the server looks up.
That definition is why the same RFC tells token servers to issue short-lived bearer tokens of one hour or less, especially for browser clients where leaks are more likely. Possession is the whole credential. If someone copies it, they are you until it expires.
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
OWASP's JWT cheat sheet points to sender-constrained tokens (DPoP or mutual TLS) as the fix: a stolen token is useless without the matching private key.
Is JWT stateless?
A JWT is stateless for verification and stateful the moment you need revocation. The server can check the signature and exp with only a key, which is genuinely stateless. But logout, banning a user, or killing a stolen token means storing something server-side, like a denylist, and every request checks it again.
This is the trap in the "JWTs scale, sessions don't" argument. Most production apps need logout that works now, not when the token expires. Once you add a denylist lookup to every request, you have rebuilt a session store with bigger tokens.
The common middle ground: a short-lived JWT access token for APIs, plus a long-lived refresh credential tracked server-side and kept in an HttpOnly cookie, so revocation happens at refresh time.
Do sessions violate REST statelessness?
Strictly, server-side sessions break REST's stateless constraint as Roy Fielding defined it. His 2000 dissertation says each request "must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. Session state is therefore kept entirely on the client."
A cookie pointing at user state stored on your server is "stored context on the server." Fielding also explains why the constraint exists: it improves visibility, reliability and scalability, at the cost of network performance, since the same data repeats on every request.
In practice, two things soften this:
- Authentication is not conversation state. A credential sent on every request is still part of "all of the information" the request needs. The clearer violation is server-held conversation state, like a multi-step form or cart kept in one server's memory.
- The goal is the property, not the label. If your session store is a shared database or Redis that every server can reach, any server can still handle any request. You keep most of the scalability Fielding was after.
For a refresher on what REST actually promises, our REST API explainer covers the constraints, and REST vs gRPC covers when you would leave REST entirely.
RS256 vs HS256: which JWT algorithm should you use?
HS256 uses one shared secret to both sign and verify, so every verifier can also mint tokens. RS256 signs with a private key and verifies with a public key, so verifiers cannot forge tokens. Use HS256 when one service issues and checks its own tokens. Use RS256 when other services or third parties verify them.
| HS256 | RS256 | |
|---|---|---|
| Type | HMAC with SHA-256 (symmetric) | RSA signature with SHA-256 (asymmetric) |
| Keys | One shared secret | Private key signs, public key verifies |
| RFC 7518 status | Required | Recommended |
| Minimum key | 256 bits per RFC 7518 (OWASP: 160+ bits of entropy) | 2048 bits per RFC 7518 |
| Who can create tokens | Anyone holding the secret | Only the private key holder |
| Best for | One backend verifying its own tokens | Many services, or third parties, verifying |
Two rules from the specs cover most real-world JWT bugs. RFC 8725 says "human-memorizable passwords MUST NOT be directly used as the key" for HMAC, so JWT_SECRET=mysecret123 is a vulnerability, not a config value. And OWASP warns about algorithm confusion, where a public key gets misused as an HMAC secret. The algorithms allowlist in the code above stops that. RFC 7518 also marks ES256 as Recommended+, meaning its requirement level is likely to rise.
Which should you pick? A decision flowchart
Pick by who verifies the user, not by which option sounds more modern. If the backend that logs users in also serves their browser requests, sessions are simpler and safer. If other services, domains or non-browser clients need to verify identity, use short-lived JWTs.
Walk through it in order and stop at the first yes:
- Is the client a browser talking to your own backend on the same site? Use a server session in an HttpOnly, Secure, SameSite cookie.
- Do other services, domains or third parties need to verify the user without calling you? Use JWTs signed with RS256 (or ES256) and publish the public key.
- Is it a mobile app, CLI or server-to-server call? Use a short-lived JWT access token in a
Bearerheader, one hour or less, plus a refresh token you can revoke. - Is it a single-page app on a different domain from its API? Put a backend-for-frontend in between that holds the token and gives the browser a session cookie, or use a JWT in an HttpOnly cookie with CSRF protection. Never
localStorage. - Whatever you picked, do you need instant logout or bans? Keep revocation server-side: sessions, or a JWT plus a denylist or refresh-token store.
| Situation | Pick |
|---|---|
| Server-rendered app, admin panel, classic web app | Session cookie |
| SPA on the same site as its API | Session cookie |
| Public API, partner integrations | JWT bearer tokens, RS256 |
| Microservices behind a gateway | JWT, verified at each service |
| Mobile or desktop app | Short-lived JWT plus revocable refresh token |
| Banking, health, anything needing an instant kill switch | Sessions with short idle timeouts |
FAQ
Is JWT more secure than session authentication?
Neither is inherently more secure. Sessions are easier to get right because the client holds only a random ID and you can revoke it instantly. JWTs add risks around algorithm checks, weak HMAC secrets and token storage, which is why RFC 8725 exists.
Can I store a JWT in a cookie?
Yes, and for browser apps it is a better place than localStorage. Mark the cookie HttpOnly, Secure and SameSite, and add CSRF protection, which RFC 6750 requires for tokens in cookies. RFC 6265 only guarantees 4,096 bytes per cookie, so keep the token small.
How long should a JWT access token last?
RFC 6750 says bearer tokens should be short-lived, one hour or less, and especially short for browser clients. Pair a short access token with a refresh token tracked server-side so you can cut access without waiting for expiry.
Should I use HS256 or RS256 for a single backend?
HS256 is fine when the same service issues and verifies its tokens, as long as the secret is a random value of at least 256 bits and never a password. Move to RS256 or ES256 as soon as another service needs to verify tokens.
Run the flowchart against your current app this week, and check the algorithms option in your JWT library while you are there. Then grab the free tools on Art of Code for the rest of your debugging, or go deeper on the format with JWT explained.
Written by Adithya Guttha, Founder of Art of Code.
Sign in to like and save posts.
Keep reading



Discussion
Comments (0)
Want to join the conversation?
Sign in to commentNo comments yet. Be the first to comment!