Securing Your APIs: JWT vs. Sessions

The choice is not really about tokens versus cookies. It is about where the source of truth for a login lives: in your data store, or in a signed string you handed to the client.
How Sessions Work
The server creates a record, gives the client an opaque identifier in a cookie, and looks the record up on every request. Revocation is a delete. Permission changes take effect immediately because the record is read fresh each time.
The cost is a lookup per request and shared state between instances. In practice a Redis lookup adds well under a millisecond, which is far less than most applications spend on their own database queries. The scaling objection is largely folklore.
How JWTs Work
The server signs a payload containing claims. Anyone holding the public key can verify it without asking the issuer. That is genuinely valuable when a request crosses trust boundaries: a gateway validating before routing, independent services accepting a caller's identity, a third party checking a token you issued.
The cost is that you cannot un-issue one. Fire someone, and their token keeps working until it expires. The standard fix is a revocation list, which reintroduces the central lookup you adopted JWTs to avoid.
The Hybrid Almost Everyone Should Use
Short-lived access token plus long-lived, revocable, rotating refresh token. You get stateless verification on the hot path and real revocation on a bounded delay.
```js
const ACCESS_TTL = '10m';function issueAccessToken(user) { return jwt.sign( { sub: user.id, roles: user.roles, ver: user.tokenVersion }, PRIVATE_KEY, { algorithm: 'RS256', expiresIn: ACCESS_TTL, audience: 'api', issuer: 'auth.example.com' } ); }
function verify(token) { return jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'], // never trust the header's alg audience: 'api', issuer: 'auth.example.com', }); } ```
The tokenVersion claim is the cheap emergency brake. Increment it on the user record during a password reset or a compromise, and every outstanding token for that user is invalid on next verification.
Mistakes That Cause Real Breaches
- Trusting the alg header. Always pin the algorithm on the verifying side. This is how signature-stripping attacks succeed.
- Storing tokens in localStorage. Any XSS becomes full account takeover. Use httpOnly, Secure, SameSite cookies and pair them with CSRF protection.
- Skipping aud and iss checks. A valid token for a different service should not open yours.
- Multi-hour access tokens. That window is exactly how long a stolen token stays useful.
- Putting sensitive data in the payload. A JWT is signed, not encrypted. Anyone can read it.
Choosing Quickly
Single backend serving your own web app: sessions. Public API with many independent consumers or a service mesh: short-lived JWTs with rotating refresh tokens. When in doubt, pick sessions, because revocation you get for free is worth more than a lookup you will never notice.
Enjoyed this article?
Share it with your network and join the conversation.