Skip to main content

Command Palette

Search for a command to run...

Sessions vs JWT vs Cookies: Understanding Authentication Approaches

Updated
13 min read

Every web app needs to remember who you are. How it does that, sessions, cookies, or tokens, shapes the entire architecture.


The Core Problem: HTTP Is Forgetful

HTTP is stateless. Every request your browser makes to a server is completely independent. The server has no memory of previous requests. No matter how many times you've visited, each new request arrives as if from a stranger.

That creates an obvious problem. When you log into a website, you don't want to re-enter your username and password on every single page. The server needs some way to recognize you on subsequent requests.

Authentication is the solution to HTTP's forgetfulness. And there are three main ingredients in most authentication systems, cookies, sessions, and tokens. They're often confused with each other, partly because they work together.

Let's understand each one separately before comparing them.


What Cookies Are

A cookie is a small piece of data that the server sends to the browser, and the browser automatically sends back on every subsequent request to that same server.

That's it. A cookie is just a key-value pair that travels back and forth between the browser and server.

Server → Browser:  Set-Cookie: userId=42; HttpOnly; Secure
Browser → Server:  Cookie: userId=42   (on every future request)

Cookies themselves are just a transport mechanism. They don't define authentication, they carry data. What data you put inside the cookie determines whether you're doing session-based auth or token-based auth.

// Setting a cookie in Node.js (Express)
response.cookie("sessionId", "abc123xyz", {
  httpOnly: true,   // JavaScript can't read it — prevents XSS theft
  secure: true,     // Only sent over HTTPS
  maxAge: 86400000  // Expires in 24 hours (milliseconds)
});
// Reading a cookie (with cookie-parser middleware)
const sessionId = request.cookies.sessionId; // "abc123xyz"

Important cookie attributes worth knowing:

  • HttpOnly — Prevents JavaScript from accessing the cookie. Protects against scripts stealing it.

  • Secure — Cookie only sent over HTTPS. Never sent over plain HTTP.

  • SameSite — Controls when cookies are sent cross-site. Helps prevent cross-site request forgery.

  • Max-Age / Expires — When the cookie should disappear.

Cookies are sent automatically by the browser on every matching request. You don't write code to include them, the browser handles it.


What Sessions Are

A session is a way for the server to remember state about a user across multiple requests. The server stores some data about you (your user ID, your permissions, your cart contents), and gives you a reference to that data, typically a random ID.

Here's how session-based authentication works step by step:

1. User submits username and password

2. Server checks credentials against database
   → valid ✓

3. Server creates a session:
   sessions["abc123xyz"] = { userId: 42, name: "Priya", role: "admin" }

4. Server sends the session ID to the browser:
   Set-Cookie: sessionId=abc123xyz

5. Browser stores the cookie and sends it on every request:
   Cookie: sessionId=abc123xyz

6. Server receives the session ID, looks it up:
   sessions["abc123xyz"] → { userId: 42, name: "Priya", role: "admin" }

7. Server knows who you are → serves the request

The cookie only contains the session ID, a meaningless random string like "abc123xyz". All the real data (who you are, what you can do) lives on the server.

// Express with express-session
const session = require("express-session");

app.use(session({
  secret: "your-secret-key",
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, maxAge: 3600000 }
}));

// Login route — create session
app.post("/login", (req, res) => {
  const user = authenticate(req.body.username, req.body.password);
  if (user) {
    req.session.userId = user.id;
    req.session.name = user.name;
    res.json({ message: "Logged in" });
  }
});

// Protected route — read session
app.get("/dashboard", (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: "Not authenticated" });
  }
  res.json({ message: `Welcome, ${req.session.name}` });
});

// Logout — destroy session
app.post("/logout", (req, res) => {
  req.session.destroy();
  res.clearCookie("sessionId");
  res.json({ message: "Logged out" });
});

Sessions are stateful, the server holds the state. The client just holds a key to look it up.


What JWT Tokens Are

A JWT (JSON Web Token) takes a fundamentally different approach. Instead of the server storing your user data and giving you a reference to it, the server encodes your user data directly into a token and gives you the whole thing.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJuYW1lIjoiUHJpeWEiLCJyb2xlIjoiYWRtaW4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

That's three base64-encoded chunks separated by dots:

HEADER.PAYLOAD.SIGNATURE

Header:    { "algorithm": "HS256", "type": "JWT" }
Payload:   { "userId": 42, "name": "Priya", "role": "admin", "exp": 1735689600 }
Signature: HMACSHA256(header + "." + payload, secretKey)

The payload is readable by anyone, it's just base64 encoded, not encrypted. You can decode it in a browser console. That's why you should never put sensitive data (passwords, credit card numbers) in a JWT payload.

The signature is what matters for security. The server created it using a secret key. When you send the token back, the server verifies the signature. If anyone tampered with the payload, changed their role from "viewer" to "admin", for example, the signature won't match and the token is rejected.

Here's how JWT authentication works:

1. User submits username and password

2. Server checks credentials against database
   → valid ✓

3. Server creates and signs a JWT:
   { userId: 42, name: "Priya", role: "admin", exp: (24 hours from now) }
   → eyJhbGci...

4. Server sends the JWT to the client

5. Client stores it (localStorage or a cookie) and sends it on future requests:
   Authorization: Bearer eyJhbGci...

6. Server receives the token, verifies the signature
   → signature valid ✓

7. Server reads the payload directly — no database lookup needed
   → userId: 42, role: admin → serves the request
const jwt = require("jsonwebtoken");

const SECRET = process.env.JWT_SECRET;

// Login — create and sign JWT
app.post("/login", (req, res) => {
  const user = authenticate(req.body.username, req.body.password);

  if (user) {
    const token = jwt.sign(
      { userId: user.id, name: user.name, role: user.role },
      SECRET,
      { expiresIn: "24h" }
    );
    res.json({ token });
  }
});

// Protected route — verify JWT
app.get("/dashboard", (req, res) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return res.status(401).json({ error: "No token" });
  }

  const token = authHeader.split(" ")[1];

  try {
    const decoded = jwt.verify(token, SECRET);
    res.json({ message: `Welcome, ${decoded.name}` });
  } catch (error) {
    res.status(401).json({ error: "Invalid or expired token" });
  }
});

JWTs are stateless, the server stores nothing. All the information needed to verify and identify the user is in the token itself.


Stateful vs Stateless Authentication

This is the fundamental difference between sessions and JWTs, and it drives almost every practical trade-off between them.

Stateful (Sessions):

The server maintains a record of every active session. When a request arrives, the server looks up the session ID in its storage, a database, Redis, or memory, to find out who's making the request.

Client          Server          Session Store
  │                │                │
  │──sessionId──▶  │                │
  │                │──lookup──────▶ │
  │                │◀──user data─── │
  │◀──response──   │                │

Stateless (JWT):

The server stores nothing. When a request arrives, the server verifies the token's signature and reads the user data directly from the token itself. No lookup required.

Client          Server
  │                │
  │──JWT token───▶ │
  │                │ (verify signature, read payload)
  │◀──response──   │

This difference is architectural, not just technical. It affects how you scale, how you handle logout, and how your infrastructure is organized.


Differences Between Session-Based Auth and JWT

Sessions JWT
Where user data lives On the server (session store) Inside the token (client-side)
What the client holds A session ID The full token with data
Server memory required Yes — stores all active sessions No — stateless
Database lookup per request Yes — to find session data No — data is in the token
Logout Easy — delete the session server-side Harder — token stays valid until expiry
Scaling across servers Requires shared session store (Redis) Easy — any server can verify any token
Token invalidation Immediate — delete the session Can't invalidate before expiry without a blacklist
Payload size Tiny — just a session ID in the cookie Larger — contains all user data
Works without cookies Typically needs cookies Can use Authorization header — better for APIs

Logout Is the Revealing Trade-off

With sessions, logout is immediate and guaranteed. You delete the session from the store. The next request with that session ID finds nothing and gets rejected.

// Session logout — immediate
req.session.destroy();
// That sessionId is now dead — can't be used again

With JWTs, logout is not inherently possible. The token was issued. The server has no record of it. Even if the user "logs out" and you delete the token from their browser, someone who copied that token before logout can still use it until it expires.

// JWT "logout" — client-side only
localStorage.removeItem("token");
// But if someone copied the token earlier, it still works until exp

To truly invalidate JWTs, you need a token blacklist, a list of revoked tokens stored somewhere the server checks on every request. But at that point, you've reintroduced server-side state, and you're approaching the session model anyway.

For short expiry times (15 minutes, 1 hour), this is often acceptable. For applications where instant logout matters, financial, healthcare, enterprise, it's a genuine constraint.


How Cookies Fit With Both Approaches

Cookies are used by both session-based and JWT-based systems, they're just carrying different things.

Session auth + cookie: The cookie holds the session ID. The real data is on the server.

JWT auth + cookie: The cookie holds the JWT token itself. The real data is in the token.

JWT auth + localStorage: No cookie, the token is stored in browser localStorage and sent manually via the Authorization header.

Using a cookie to store a JWT gives you some security advantages, specifically, HttpOnly cookies can't be accessed by JavaScript, which protects against certain attacks. But it comes with its own trade-offs around cross-site requests.


When to Use Each Method

Use Sessions When:

You're building a traditional server-rendered web app. Sessions are the natural fit here. Users log in, the server remembers them, and every page request includes the session cookie automatically. No extra work needed.

You need immediate logout. If your application requires that logging out actually works — that an admin can revoke a user's access instantly, sessions make this trivial.

You're running on a single server. Sessions stored in memory are dead simple and have no additional dependencies. For a small app, this is often the right call.

Your users are on a web browser. Cookies work seamlessly with browsers. No client-side token management code needed.

Use JWT When:

You're building an API consumed by mobile apps or third-party clients. JWT tokens are sent via the Authorization header, which works naturally across platforms, mobile apps, CLIs, external services. Cookies don't travel as cleanly outside browsers.

You need to scale horizontally. If your service runs across multiple servers, or on serverless functions, you don't want to manage a shared session store. JWTs let every server verify tokens independently with no shared infrastructure.

You're working with microservices. A JWT issued by your auth service can be verified by any other service without that service calling back to auth. The token carries the identity.

Your sessions are short-lived. JWTs work best when expiry times are short, 15 minutes to a few hours. Short expiry reduces the logout problem to something manageable.

Practical Combinations:

Most real applications don't choose one approach and ignore the other. Common patterns:

Traditional web app:    Sessions + cookies
REST API:               JWT in Authorization header
Mobile app backend:     JWT with refresh token pattern
Hybrid web + API:       Cookie-stored JWT (gets the best of both)

A Realistic Decision Framework

Is this a web browser app?
  ├── Yes → Sessions are simple and battle-tested
  │          Use JWT only if you have a specific reason to
  │
  └── No (API, mobile, microservices)
        ├── Do you need instant logout?
        │     └── Yes → Consider sessions with shared store
        │                or JWT + token blacklist
        │
        ├── Do you need to scale across multiple servers?
        │     └── Yes → JWT is a strong fit
        │
        └── Are sessions short? (under 1 hour)
              └── Yes → JWT with short expiry is likely fine

Key Takeaways

  • HTTP is stateless — authentication exists to give it memory across requests.

  • Cookies are a transport mechanism — they carry data between browser and server automatically.

  • Sessions store user data on the server; the client holds only a session ID.

  • JWT stores encoded user data in a token on the client; the server holds nothing.

  • Stateful (sessions) = server remembers you. Stateless (JWT) = token proves who you are.

  • Sessions make logout easy — delete the session server-side. JWT logout is hard without a blacklist.

  • JWT makes horizontal scaling easy — any server can verify any token. Sessions need shared storage.

  • JWTs work well for APIs and mobile apps. Sessions work well for server-rendered browser apps.

  • Short JWT expiry times reduce the logout problem significantly.

  • Most real applications use a combination of these approaches depending on the use case.


Quick Reference

// ── Cookie (Express) ──────────────────────────────────
res.cookie("name", "value", {
  httpOnly: true,
  secure: true,
  sameSite: "strict",
  maxAge: 86400000
});
const value = req.cookies.name;

// ── Session (express-session) ─────────────────────────
req.session.userId = user.id;     // set
const id = req.session.userId;    // get
req.session.destroy();            // logout — immediate

// ── JWT (jsonwebtoken) ────────────────────────────────
// Create
const token = jwt.sign({ userId: 42 }, SECRET, { expiresIn: "1h" });

// Verify
try {
  const payload = jwt.verify(token, SECRET);
  // payload.userId = 42
} catch (err) {
  // invalid or expired
}

// Send in request header
Authorization: Bearer <token>

// ── When to use ───────────────────────────────────────
// Session:  browser app, instant logout needed, single server
// JWT:      API/mobile, multi-server, microservices, short sessions
// Cookie:   transport layer — used by both approaches in browser context

Sessions remember you on the server. JWTs prove who you are with the token itself. Cookies just make sure the right thing gets delivered to the right place.