JWT vs session authentication: which should you use?
Sessions end a login instantly; JWTs skip the database lookup. How each works, why revocation decides it for device limits and forced logout, and a hybrid for web and apps.
On this page 8 sections
Use server-side sessions when you need to end a login instantly, and JWTs when many services must verify a user without a shared lookup; most course platforms need the first more than the second. A session is a random ID pointing to a record on your server, so deleting the record ends access at once. A JWT is a signed token that servers check without any lookup, so it stays valid until it expires unless you add a revocation check, which is exactly what device limits and account-sharing controls require.
How sessions work
- Login. After checking the password against its stored hash (see our guide to password hashing) and any OTP, the server creates a random session ID and stores a record: which student, which device, when it started, when it was last used.
- Cookie. The browser receives the ID in a cookie marked
Secure,HttpOnlyandSameSite, so it travels only over HTTPS and page scripts can't read it. - Every request. The server looks the ID up. No record, no access.
- Logout or revocation. Delete the record, and the very next request fails.
The ID itself carries no information, so it can't be forged or decoded; the OWASP session management guidance asks for at least 64 bits of randomness. The costs are a lookup per request and a shared store, usually the database or an in-memory store such as Redis, that every app server can reach. Cookie-based sessions also need protection against cross-site request forgery, which SameSite cookies and CSRF tokens provide.
How JWTs work
A JSON Web Token, defined in RFC 7519, is three Base64url-encoded parts joined by dots: a header naming the algorithm, a payload of claims, and a signature. Decoded, a typical payload looks like this:
{
"sub": "student_48213",
"sid": "d7f3a91c",
"aud": "api",
"iat": 1790640000,
"exp": 1790640900
}
The server signs it, either with a shared secret (HS256, an HMAC) or with a private key (RS256, ES256 or EdDSA). Any service holding the right key can then check the signature and the expiry time without asking a database. That is the attraction.
Two misunderstandings cause many JWT mistakes:
- A JWT is not encrypted. Anyone who has the token can decode the payload in seconds; Base64url is an encoding, as our guide to whether Base64 is encryption explains. Never put phone numbers, fee details or secrets in it. When people search for how to "decrypt" a JWT, they almost always mean decode it.
- The token tells the server how to check it, through the "alg" header. RFC 8725, the JWT best-practice guide, records attacks where libraries accepted "none" as the algorithm, or checked an RS256 token as HS256 using the public key as the secret. Always fix the allowed algorithms on the server, and validate the issuer and audience.
Revocation: the real difference
| Event | Server-side session | JWT with no server check |
|---|---|---|
| Student logs out | Record deleted; access ends at once | A copied token keeps working until it expires |
| Password changed after a suspected leak | All sessions deleted at once | Old tokens stay valid until expiry |
| Device limit exceeded | Delete the oldest device's session | No way to reach tokens already issued |
| Account blocked for selling access | Immediate | Only when the last token expires |
There are three standard ways to make JWTs revocable: keep them short-lived and pair them with refresh tokens; keep a denylist of revoked token IDs; or store a per-user token version and reject older tokens. Each of the last two needs a lookup on every request, which is a session store by another name. The honest summary: JWTs save lookups only where you can tolerate the delay before a revocation takes effect.
For refresh tokens, the OAuth security best-practice document, RFC 9700, requires that tokens given to apps and browser clients are either bound to the device cryptographically or rotated on every use. With rotation, if a stolen refresh token and the genuine one are both used, the server sees an old token come back and revokes the whole session.
Mobile apps and APIs
- Apps usually send tokens in an Authorization header rather than cookies. Store them in the Keychain on iOS and in Keystore-backed storage on Android, not in plain files or preferences.
- Browsers should keep credentials out of JavaScript's reach. OWASP's guidance is not to store session IDs, JWTs or refresh tokens in localStorage or sessionStorage, and to use HttpOnly, Secure, SameSite cookies or a backend-for-frontend instead.
- JWTs shine where many independent services or edge servers must check a user quickly: service-to-service calls, single sign-on, and short-lived tokens that a CDN checks before serving a file, much like the signed URLs used for hotlink protection.
- Sessions shine in a single web application with one database, which describes most course websites. Frameworks such as Django, Rails and Laravel handle them well by default.
Device limits and forced logout
A worked example, with illustrative numbers. An institute allows two devices per student and logs out the oldest when a third one signs in. Rahul signs in on a friend's laptop at 7:55 p.m., five minutes before a live class. How long does his oldest device keep working?
| Design | Oldest device keeps access for | Server lookups |
|---|---|---|
| Server-side session, checked on every request | Until its next request, usually seconds | One per request |
| JWT valid for 24 hours, never checked | Up to 24 hours | None |
| JWT valid for 15 minutes, with rotating refresh tokens | Up to 15 minutes | One per refresh |
| JWT valid for 15 minutes, plus a session check on paid-content and test requests | Paid content stops at its next request; other calls within 15 minutes | One per paid-content or test request |
The second row is the trap. With 24-hour tokens, a device limit is decoration: a login shared at breakfast works for the whole group until the next morning. Our guide to stopping account sharing covers the policy side; this is the mechanism that makes the policy enforceable.
A hybrid that works
For a platform with a website and apps, this design gives instant control where it matters and cheap checks everywhere else:
- Keep a server-side record for every signed-in device: session ID, student, device ID, created, last seen, revoked.
- Issue a short-lived access token, valid for 5 to 15 minutes, JWT or opaque, carrying the session ID.
- Issue an opaque refresh token, stored only as a hash on the server, tied to the session and rotated on every use. Reuse of an old one revokes the session.
- Check the session record at high-value moments, whatever the token says: opening paid videos or notes, starting or submitting a test, paying fees and changing devices.
- Revoke by marking the record. "Log out other devices" then stops refreshes and high-value requests immediately, and leftover access tokens die within minutes.
The session check costs one fast lookup at the moments that matter, while routine requests are handled from the token alone. It is the same trade-off as before, applied selectively: instant revocation where a delay would be costly, and cheap verification everywhere else.
Key takeaways
- Sessions are revocable instantly but need a lookup; JWTs skip the lookup but stay valid until they expire.
- JWTs are signed and Base64url-encoded, not encrypted, so never put secrets in them.
- Pin the allowed algorithms and validate issuer, audience and expiry on every JWT.
- Long-lived JWTs make device limits and forced logout meaningless.
- A hybrid of short-lived access tokens, rotating refresh tokens and session checks at high-value moments works well for web and apps.
Frequently asked questions
Is JWT encrypted?
Usually not. Most JWTs are signed, which proves they weren't altered, but the header and payload are only Base64url-encoded, so anyone holding the token can read them. Encrypted JWTs exist, using the JWE format with five dot-separated parts instead of three, but they are less common. Treat an ordinary JWT as readable by anyone, keep sensitive data out of it, and always send it over HTTPS.
Is JWT stateless?
Checking a JWT is stateless: a server needs only the key to verify the signature and expiry, with no database lookup. Real systems rarely stay fully stateless, though, because stateless tokens can't be revoked before they expire. Refresh tokens, denylists, token versions and session checks for sensitive actions all add server state back, trading a lookup for the ability to log users out.
Is JWT encoded or encrypted?
A standard JWT is encoded and signed, not encrypted. The header and payload are JSON converted to Base64url text, which anyone can decode without a key; the signature lets the server detect tampering. Only JWE tokens are encrypted. So when someone asks how to decrypt a JWT, the answer is usually that there's nothing to decrypt, just text to decode.
Is JWT Base64 encoded?
Yes, in a URL-safe variant called Base64url. It swaps "+" and "/" for "-" and "_" and drops the "=" padding, so tokens can sit safely in URLs and headers. The header and payload are Base64url-encoded JSON, and the signature is Base64url-encoded bytes. Some ordinary Base64 decoders need the padding added back before they can read a JWT.