Skip to main content

Command Palette

Search for a command to run...

Sessions vs JWT vs Cookies: Understanding Authentication Approaches

Understanding authentication approaches · stateful vs stateless · when to use each

Published
7 min read
Sessions vs JWT vs Cookies: Understanding Authentication Approaches

Every web application needs to know who is making a request. But HTTP is stateless — it has no memory. Each request arrives blank. Authentication is the engineering discipline of solving this amnesia: how do we remember that this user logged in five minutes ago? Three approaches dominate the modern web: sessions, cookies, and JWT tokens. They're often conflated, sometimes combined, and frequently misunderstood



Common misconception: Cookies and sessions are not the same thing. A cookie is just a place to store data in the browser. Sessions typically use a cookie to store the session ID — but sessions can also live in URLs or local storage. A JWT can also be stored in a cookie. These are layered concepts, not alternatives at the same level.


Sessions Explained

01 / What Are Sessions

When a user logs in, the server creates a session record in its memory or a database — something like { sessionId: "abc123", userId: 42, role: "admin", expiresAt: … }. The server then sends only the session ID back to the client. On every subsequent request, the client sends that ID, and the server looks it up to retrieve the user's identity.

This is stateful authentication. The server must remember state between requests. The ID itself is meaningless — it's just a pointer to real data sitting on the server side.

// 1. User logs in — server creates a session
app.post('/login', (req, res) => {
  const user = validateCredentials(req.body);
  if (user) {
    req.session.userId = user.id;   // stored server-side
    req.session.role   = user.role;
    res.json({ message: 'Logged in' });
    // Browser automatically gets Set-Cookie: sid=abc123
  }
});

// 2. Protected route — server looks up session by cookie
app.get('/dashboard', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  res.json({ userId: req.session.userId });
});

Cookies: The Delivery Layer

02 / What Are Cookies

A cookie is simply a piece of data the server asks the browser to store — sent via the Set-Cookie HTTP header. The browser then automatically attaches it to every subsequent request to that domain via the Cookie header.

Cookies are a storage and transport mechanism, not an authentication system. Sessions commonly use cookies to store the session ID. JWTs can also be stored in cookies. Cookies can hold any string — they're the pipe, not the water.

# Server sends this response header:
Set-Cookie: sessionId=abc123;
            HttpOnly;        # JS cannot read this cookie
            Secure;          # HTTPS only
            SameSite=Strict; # No cross-site requests
            Max-Age=86400    # Expires in 24 hours

# Browser automatically sends on every request:
Cookie: sessionId=abc123
💡
A cookie is just a string the browser carries. Whether that string is a session ID, a JWT, or a user preference — the cookie doesn't care.

JWT: Identity in the Token

03 / What Are JWT Tokens

A JSON Web Token is a self-contained credential. Instead of issuing a random ID and keeping a record server-side, the server encodes the user's identity directly into the token itself — and signs it cryptographically so it can't be tampered with.

A JWT has three parts separated by dots: a header (algorithm info), a payload (claims: user ID, role, expiry), and a signature (cryptographic proof of authenticity). When the server receives a JWT, it only needs to verify the signature — no database lookup required.

const jwt = require('jsonwebtoken');

// 1. Login: sign a token and return it
app.post('/login', (req, res) => {
  const user = validateCredentials(req.body);
  const token = jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );
  res.json({ token });  // client stores this
});

// 2. Protected route: verify without any DB call
app.get('/dashboard', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  res.json({ userId: decoded.userId });
});

Stateful vs Stateless Auth

04 / Core Concept

This is the architectural divide. Stateful authentication (sessions) requires the server to maintain a record of who is logged in. Every request triggers a database or cache lookup. The server is the source of truth — which means it can also instantly revoke access.

Stateless authentication (JWT) embeds identity in the token itself. The server trusts the token's signature and needs no external lookup. This scales beautifully — any server in a cluster can validate any token independently. The tradeoff: you cannot "un-sign" a token once issued. Revocation requires workarounds like a token blocklist (which reintroduces state).

Session-Based vs JWT: Comparison

05 / Differences

Property Sessions JWT Tokens Cookies (as transport)
State Stateful — server stores session data Stateless — token carries all data Neutral — just a transport mechanism
Storage Session store (Redis, DB, memory) Client-side (localStorage, cookie) Browser, auto-sent with requests
Revocation Instant — delete session record Hard — must wait for expiry or blocklist Depends on what it carries
Scalability Requires shared session store across servers Scales naturally — any server can verify No scaling impact on its own
Payload size Small (just an ID in the cookie) Larger — grows with claims in token Limited to ~4KB total
Server DB call Every request hits the session store No lookup needed — verify in memory Depends on what the cookie holds
Cross-domain Difficult — cookies are domain-scoped Easy — bearer token works anywhere Restricted by SameSite / CORS rules
Typical use Traditional web apps, server-rendered APIs, mobile apps, microservices Paired with sessions or JWT for transport

When to Use Each

06 / Decision Guide

There is no universally superior approach. The right choice depends on your architecture, your team's needs, and the nature of your application.

Use Sessions When

  • You need instant account revocation (e.g., "log out all devices", ban a user)

  • Building a traditional server-rendered web app (Rails, Django, Express with Pug)

  • Your data is sensitive and you want zero user data in the browser

  • You already run Redis or a caching layer

  • You're building an admin panel or internal tool with strict access control

Use JWT When

  • Building a stateless REST API consumed by mobile apps or SPAs

  • Operating across multiple domains or microservices

  • Horizontal scaling matters and a shared session store is a bottleneck

  • Implementing OAuth 2.0 or third-party integrations

  • Short-lived tokens where expiry-based invalidation is acceptable

Use Cookies (as transport) When

  • You want the browser to handle token delivery automatically

  • Security is a priority — HttpOnly cookies block XSS token theft

  • You want SameSite=Strict CSRF protection built in

  • The app and API share the same domain

Combine Both

  • Store a JWT inside an HttpOnly cookie to get stateless verification with the security of cookie transport

  • Use a short-lived JWT + a long-lived refresh token in a session record for revocable stateless auth

  • Many production apps pair these approaches depending on route sensitivity

💡
The practical default for most web apps in 2025: JWT stored in an HttpOnly, Secure, SameSite=Strict cookie gives you stateless verification, automatic browser delivery, and meaningful XSS protection — without the scaling overhead of a session store.