Skip to main content

Command Palette

Search for a command to run...

Sessions vs JWT vs Cookies: Understanding Authentication Approaches

Updated
11 min read
Sessions vs JWT vs Cookies: Understanding Authentication Approaches
H
CS undergrad | Tech enthusiast | Focusing on Web Dev • DSA • ML | Building skills for real-world impact

You'll see sessions, JWT, and cookies mentioned together a lot, but they're not the same thing. A session is how you track a user. A cookie is how you deliver that tracking. JWT is an alternative way to track users. They solve the same problem in different ways.

Choose wrong and you'll spend weeks bolting things together that don't fit. Get it right and authentication just works.

This is about understanding what each approach actually does and when to use it.


What Sessions Are

A session is server-side memory about a user. When they log in, the server creates a session and stores their info (user ID, role, etc.). On every request, the server looks up that session.

Session Flow

Login:
1. User sends username and password
2. Server checks credentials
3. Server creates a session object
4. Server stores session
5. Server sends back session ID

Next Request:
1. User sends session ID
2. Server looks it up
3. Server knows who they are
4. Request is allowed

Real Code

const sessions = {}; // Session store

app.post("/login", (req, res) => {
  const { username, password } = req.body;
  
  if (username === "alice" && password === "secret123") {
    const sessionId = Math.random().toString();
    sessions[sessionId] = {
      userId: 1,
      username: "alice",
      loginTime: new Date()
    };
    
    res.json({ sessionId });
  } else {
    res.status(401).json({ error: "Invalid credentials" });
  }
});

app.get("/profile", (req, res) => {
  const sessionId = req.query.sessionId;
  const session = sessions[sessionId];
  
  if (session) {
    res.json({ message: `Welcome, ${session.username}!` });
  } else {
    res.status(401).json({ error: "Not logged in" });
  }
});

The server stores everything. It remembers the user on every request.


What Cookies Are

A cookie is a small file stored on the browser. The server tells the browser to store it, and the browser automatically sends it with every request.

Cookies don't do authentication themselves. They're a container. You typically put a session ID or JWT token in a cookie so the browser auto-sends it without manual work.

How It Works

Server response:
Set-Cookie: sessionId=abc123; HttpOnly

Browser stores this cookie.

Next request:
Browser automatically includes: Cookie: sessionId=abc123
Server receives it and knows who the user is

Real Code

app.post("/login", (req, res) => {
  const { username, password } = req.body;
  
  if (username === "alice" && password === "secret123") {
    const sessionId = generateSessionId();
    sessions[sessionId] = { userId: 1, username: "alice" };
    
    res.cookie("sessionId", sessionId, {
      httpOnly: true,      // Can't be accessed from JavaScript
      secure: true,        // Only sent over HTTPS
      sameSite: "strict",  // CSRF protection
      maxAge: 1000 * 60 * 60 * 24 // 24 hours
    });
    
    res.json({ message: "Logged in" });
  }
});

app.get("/profile", (req, res) => {
  const sessionId = req.cookies.sessionId;
  const session = sessions[sessionId];
  
  if (session) {
    res.json({ user: session.username });
  } else {
    res.status(401).json({ error: "Not logged in" });
  }
});

The browser handles it. You don't have to send the session ID manually.


What JWT Tokens Are

JWT (JSON Web Token) is a token format that contains encoded data. The token itself holds all the info. The server doesn't store anything. It just verifies the token hasn't been tampered with.

Token Structure

A JWT is three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWxpY2UifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

header.payload.signature

The server signs it with a secret key. When the user sends it back, the server verifies the signature.

Real Code

const jwt = require("jsonwebtoken");
const SECRET = "your-secret-key";

app.post("/login", (req, res) => {
  const { username, password } = req.body;
  
  if (username === "alice" && password === "secret123") {
    const token = jwt.sign(
      { userId: 1, username: "alice" },
      SECRET,
      { expiresIn: "24h" }
    );
    
    res.json({ token });
  }
});

app.get("/profile", (req, res) => {
  const token = req.headers.authorization?.split(" ")[1];
  
  if (!token) {
    return res.status(401).json({ error: "No token" });
  }
  
  try {
    const decoded = jwt.verify(token, SECRET);
    res.json({ user: decoded.username });
  } catch (err) {
    res.status(401).json({ error: "Invalid token" });
  }
});

The server never stores anything. The token proves who the user is.


Stateful vs Stateless Authentication

This is the core difference between sessions and JWT.

Stateful (Sessions)

The server remembers. It stores session data and looks it up on every request.

User logs in → Server creates session → Server stores session
User makes request → Server looks up session → Allows request

Pro: Server has full control. Can revoke access immediately. Con: Server needs storage. Doesn't scale well across multiple servers.

Stateless (JWT)

The client carries the proof. The server just verifies it.

User logs in → Server creates token → Server sends token
User makes request → User sends token → Server verifies token

Pro: Server stores nothing. Works across multiple servers easily. Con: Can't revoke tokens immediately (they're valid until they expire).


Session Authentication Flow

Client                              Server
  |                                   |
  |--- POST /login ----------------->|
  |    (username, password)           |
  |                                   |
  |                            (verify credentials)
  |                            (create session)
  |                            (store in database)
  |                                   |
  |<--- Set-Cookie: sessionId --------|
  |                                   |
  |--- GET /profile ---------------->|
  |    (Cookie: sessionId)            |
  |                                   |
  |                            (lookup session)
  |                            (check if valid)
  |                                   |
  |<--- 200 OK (profile data) --------|
  |                                   |

The server looks up the session ID and finds all the user data.


JWT Authentication Flow

Client                              Server
  |                                   |
  |--- POST /login ----------------->|
  |    (username, password)           |
  |                                   |
  |                            (verify credentials)
  |                            (create JWT token)
  |                            (sign with secret)
  |                                   |
  |<--- { token: "eyJ..." } ---------|
  |                                   |
  |--- GET /profile ---------------->|
  |    (Authorization: Bearer token)  |
  |                                   |
  |                            (verify signature)
  |                            (decode payload)
  |                                   |
  |<--- 200 OK (profile data) --------|
  |                                   |

The server never stores anything. It just verifies the token is real.


Session vs JWT Comparison

Aspect Session JWT
Storage Server stores session data Token carries all data
Lookup Server queries database Server verifies signature
Revocation Immediate (delete session) Delayed (until expiration)
Scalability Needs shared session store Works across servers
Cookie friendly Yes, stores ID in cookie Usually sent as header
Payload size Small (just ID) Larger (contains data)
Best for Web apps with server APIs and microservices
Logout Delete session, immediately effective Token stays valid until expiration

When to Use Sessions

Use sessions for traditional web applications on a single server.

Scenario 1: Web app with server-rendered HTML

// User logs in
// Server creates session
// Browser stores session ID in cookie
// Works great for this setup

Scenario 2: You need immediate logout

// User's permissions change
// Delete the session
// User is immediately logged out
// JWT tokens stay valid until they expire

Scenario 3: One server, one database

// All sessions in one place
// Simple to implement
// No distributed complexity

Scenario 4: Store lots of user data

// User's role, permissions, preferences
// Store it all in the session
// Don't repeat it in every response

Session Code Example

const express = require("express");
const session = require("express-session");

app.use(session({
  secret: "keyboard cat",
  resave: false,
  saveUninitialized: true
}));

app.post("/login", (req, res) => {
  const user = findUser(req.body.username);
  if (user && verifyPassword(req.body.password, user.password)) {
    req.session.userId = user.id;
    req.session.username = user.username;
    res.json({ message: "Logged in" });
  }
});

app.get("/profile", (req, res) => {
  if (req.session.userId) {
    res.json({ user: req.session.username });
  } else {
    res.status(401).json({ error: "Not logged in" });
  }
});

Straightforward and works well.


When to Use JWT

Use JWT for APIs, mobile apps, and distributed systems.

Scenario 1: Mobile app

// Mobile app logs in
// Gets JWT token
// Stores it locally
// Sends it with every API request
// Mobile doesn't handle cookies well

Scenario 2: Multiple clients use the same API

// Web app, mobile app, desktop app
// All hit the same API
// All get JWT tokens
// Validated the same way

Scenario 3: Microservices

// Auth service creates token
// User service validates it
// Payment service validates it
// No service needs to call another to verify

Scenario 4: Multiple independent servers

// Server 1 generates token
// User sends request to Server 2
// Server 2 verifies token without calling Server 1
// Servers are completely independent

JWT Code Example

const jwt = require("jsonwebtoken");
const SECRET = "super-secret-key";

app.post("/login", (req, res) => {
  const user = findUser(req.body.username);
  if (user && verifyPassword(req.body.password, user.password)) {
    const token = jwt.sign(
      { userId: user.id, username: user.username },
      SECRET,
      { expiresIn: "24h" }
    );
    res.json({ token });
  }
});

app.get("/profile", (req, res) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "No token" });
  
  try {
    const decoded = jwt.verify(token, SECRET);
    res.json({ user: decoded.username });
  } catch (err) {
    res.status(401).json({ error: "Invalid token" });
  }
});

Works well for APIs and distributed systems.


Cookies Can Hold Either

Important: Cookies are just a transport mechanism. You can store a session ID in a cookie (session-based) or a JWT in a cookie (JWT-based).

res.cookie("sessionId", sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: "strict"
});
// Browser auto-sends cookie, server looks up session
res.cookie("auth", jwtToken, {
  httpOnly: true,
  secure: true,
  sameSite: "strict"
});
// Browser auto-sends cookie, server verifies signature

The difference is what the server does when it receives the cookie.


Decision Tree: Which One to Use?

Ask yourself these questions:

Is this a traditional web app with a server?

  • If yes: Sessions
  • If no: Next question

Do you have one server or multiple?

  • One server: Sessions are simpler
  • Multiple servers: JWT (no shared state needed)

Do you need immediate logout?

  • Yes: Sessions (delete the session immediately)
  • No: JWT is fine

Are mobile apps consuming your API?

  • Yes: JWT (mobile doesn't use cookies well)
  • No: Sessions work

Do you have microservices?

  • Yes: JWT (services don't need to talk to auth service)
  • No: Sessions are probably fine

Practice Assignment

1. Implement session-based authentication:

const express = require("express");
const session = require("express-session");

const app = express();
app.use(session({
  secret: "my-secret",
  resave: false,
  saveUninitialized: true
}));

// Create login endpoint
// Create protected profile endpoint
// Test with curl or Postman

2. Implement JWT-based authentication:

const jwt = require("jsonwebtoken");
const SECRET = "jwt-secret";

// Create login endpoint that returns JWT
// Create protected endpoint that verifies JWT
// Store token in Authorization header
// Test with curl or Postman

3. Implement JWT in a cookie:

// Log in user
// Set JWT token in httpOnly cookie
// Create protected endpoint that reads from cookie
// Verify the JWT

4. Compare the three approaches:

// Write a simple CLI tool that tests:
// - Session-based authentication
// - JWT with Authorization header
// - JWT in a cookie
// Measure response time and storage requirements

Quick Recap

  • Sessions store user info on the server. Client gets an ID.

  • Cookies are browser files. Usually hold session IDs or JWT tokens.

  • JWT is a token format containing encoded data. No server storage needed.

  • Stateful (sessions): Server remembers everything.

  • Stateless (JWT): Client carries proof, server just verifies.

  • Use sessions for web apps on one server.

  • Use JWT for APIs, mobile apps, and multiple servers.

  • You can put JWT in a cookie, or send it as a header.

  • Sessions allow immediate logout. JWT tokens stay valid until expiration.

  • Choose based on your architecture, not on what "sounds better."

Get it right from the start. If you pick sessions and later need multiple servers, you'll regret it. If you pick JWT for a simple web app, you've added complexity you don't need. Know your architecture before you pick the authentication method.

Happy coding! 🚀


If you enjoyed this article, check out my other blogs on this profile. 🔗 Connect with me: LinkedIn | GitHub | X (Twitter)