Back to Articles
Security2026-04-259 min read

Sessions vs JWT — They Are Not Alternatives, They Solve Different Problems

Every developer has seen the debate. Sessions or JWT? The truth is that framing the question that way misses the point entirely.

JWTSessionsAuthSecurityNode.js

Sessions vs JWT — They Are Not Alternatives, They Solve Different Problems

Ask any developer which they prefer for authentication, sessions or JWT and you will get a strong opinion either way. What you will rarely get is an explanation of why the question itself is slightly wrong.

Sessions and JWT are not competing solutions to the same problem. They have different strengths, different weaknesses, and they serve different client types well. Treating them as interchangeable alternatives is what leads to systems that work for one type of client but handle the other poorly.

I built a system that uses both. Here is what I learned.

What Sessions Actually Are

When a user logs in and the server creates a session, what is actually happening is straightforward. The server generates a random identifier, stores the user's data against that identifier in a database or in memory, and sends the identifier to the browser as a cookie.

On every subsequent request, the browser sends the cookie automatically. The server looks up the identifier in the session store, retrieves the user data, and knows who is making the request.

The session ID is simply a reference. All the important details, like the user’s identity, role, and login time, are stored securely on the server.

This has two important consequences.

The first is that the server has complete control. If you need to log someone out, you delete the session. If you need to force re-authentication across the board, you clear the session store. If a session needs to be invalidated immediately — because of suspicious activity or an admin decision — it is gone the moment you delete the record. There is no window of exposure.

The second is that sessions do not travel well. The session data lives on the server that created it. If you have multiple servers, you need a shared session store. If you need a mobile app to authenticate, you need to deal with cookie handling that is not native to mobile environments. If you want a third-party service to verify a user's identity, you cannot hand it a session ID and expect it to know what to do.

What JWT Actually Is

A JSON Web Token is a self-contained signed object. It has a header, a payload containing claims like the user's ID and role, and a cryptographic signature. The server generates it, signs it with a secret key, and sends it to the client. The client stores it and sends it back in the Authorization header of future requests.

The server does not need to store anything. When a request arrives with a JWT, the server validates the signature and reads the claims directly from the token. No database lookup. No shared infrastructure required.

This is what people mean when they say JWT is stateless: all the user’s information and authentication state is contained within the token itself, not stored on the server.

The main drawback is that you can’t revoke a JWT before it expires. If someone steals a token, they can use it until it naturally runs out. Short expiry times and refresh token rotation help, but this stateless design is also its biggest limitation.

Why They Serve Different Clients

Sessions and JWTs are designed for different types of clients.

For browser-based apps, sessions are a natural fit. Browsers handle cookies automatically, and features like HttpOnly and SameSite make sessions secure and easy to manage.

For mobile apps and API clients, JWTs are a better choice. These clients don’t use browser cookies, but they can easily store a token and send it in an Authorization header. JWTs also make it simple to scale your servers without sharing session data.

Trying to use one model for both can cause problems. JWTs in browsers can be exposed to XSS or CSRF risks, while sessions in mobile apps require extra work to handle cookies. Each method works best in its intended environment.

The Hybrid Approach

The system I built serves both client types from a single backend. Browser clients get session cookies. Mobile and API clients get JWT access tokens with refresh token rotation. The same route handlers work for both.

To support both browser and API/mobile clients, the authentication middleware works in two steps:

  1. It first checks for a session cookie (used by browsers). If a valid session is found, the user is authenticated.
  2. If there’s no valid session, it checks for a JWT in the Authorization header (used by mobile apps and APIs). If the token is valid, the user is authenticated.

In both cases, the user information is attached to the request, so the rest of your code can treat all authenticated users the same way—no matter how they logged in.

Here’s what this looks like in code:

export const authenticate = async (
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  // Check session first (browser clients)
  if (req.session?.userId) {
    req.user = await getUserById(req.session.userId);
    return next();
  }

  // Fall back to JWT (mobile and API clients)
  const authHeader = req.headers.authorization;
  if (authHeader?.startsWith("Bearer ")) {
    const token = authHeader.substring(7);
    const payload = verifyAccessToken(token);
    if (payload) {
      req.user = await getUserById(payload.userId);
      return next();
    }
  }

  return res.status(401).json({ message: "Unauthorized" });
};

This approach avoids compromise: browser clients benefit from secure sessions, API clients enjoy the flexibility of JWTs, and both get the experience best suited to their needs—without some kind of extra workarounds or security trade-offs.

On Refresh Token Rotation

One important best practice with JWTs is to rotate refresh tokens every time they’re used.

When an access token expires, the client sends a refresh token to get a new one. If you always issue a new refresh token and invalidate the old one, a stolen token can only be used once. This greatly reduces the risk—if an attacker steals a refresh token, whichever party uses it first (the attacker or the real user) will invalidate the other’s token. The legitimate user will notice they’ve been logged out, signaling something is wrong.

Storing refresh tokens in the database (instead of as stateless signed tokens) also lets you revoke all of a user’s sessions at once, such as after a password change. This gives you the same level of control you’d expect from session-based authentication, but for JWTs.

The Short Version

Use sessions for browser clients. Use JWT for API and mobile clients. If you need to serve both from the same backend, combine them through a unified middleware layer.

The debate about which is better misses the point. The better question is which client types you are serving and which model serves each of them well. Most of the time, the answer is not one or the other — it is both.

Related Articles

Continue reading

More writing on security, backend systems, architecture, and practical development.

Security
6 min read

Building a Hybrid Auth System (Sessions + JWT)

Why most systems choose one authentication strategy — and how I designed a system that supports both securely and cleanly.

AuthJWTSessions
Security
8 min read

How I Built an Automated Web Security Scanner — How It Works and How Your Website Benefits From It

Most websites have security loopholes their owners do not know about. I built a tool that finds them automatically — here is how it works and what it checks.

SecurityTypeScriptNode.jsExpressOWASPDMARCSSL
Security
9 min read

Developers Are the First Line of Defence — What Secure SDLC Actually Means

Security conversations tend to focus on the user. But many of the real problems start much earlier — in the decisions developers make during design, development, and deployment.

SSDLCSecuritySTRIDEOWASPSecure Development
Related Project

See the project behind the thinking

This article connects directly to practical backend architecture and secure system design work.

Node.jsTypeScriptPostgreSQLExpressJWTRBACSecurity

Reusable Secure Auth System

A production-deployed authentication and authorization backend built under Secure SDLC principles. Supports hybrid authentication for browser and API clients, role-based access control, and a full security documentation suite.