Sessions vs JWT vs Cookies: Understanding Authentication Approaches

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).
Session ID in Cookie
res.cookie("sessionId", sessionId, {
httpOnly: true,
secure: true,
sameSite: "strict"
});
// Browser auto-sends cookie, server looks up session
JWT in Cookie
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)




