Sessions vs JWT vs Cookies: Understanding Authentication Approaches

I like making things with code. This is where I share my projects and the bugs I ran into.
Some time back I went to AIIMS Jodhpur, that is the All India Institute of Medical Sciences, for a general checkup. And honestly, kya baat hai, the whole experience was surprisingly nice. So many staff, tons of MBBS students who had cracked NEET, the national medical entrance exam, doing their rounds, and even the shuttle bus had AC, wow, kahi ghoomne jao itna acha treatment nahi milta. I chatted with a few of the students, really warm, relatable people, and they even guided me on how to do the appointment process.
So I went to a particular counter, gave my details, and they handed me an OPD number, basically a card. OPD just means Outpatient Department, the walk-in checkup section. They told me, come tomorrow at 11 sharp, or wait 3 to 4 hours today. Obviously I picked tomorrow morning. Next day I reached, and here is the part I want you to notice. Before the doctor, I first went to the counter, and they asked me for the card and verified it, pulled up my details, and only then the doctor called me in, exactly at 11. He checked me with a few instruments, said I was perfectly fine, it was just the crazy heat and dry skin, told me to use a face wash and sunscreen, chatted for about four minutes, and advised me to carry an umbrella in the daytime. Bahut hi sweet experience.
Now think about how AIIMS actually recognised me. They kept my file on their side, and gave me just a card, a plain number that pointed to that file. Every time, I showed the card, they looked me up. That is a session. Compare it with my Taj key from the last post, which carried everything inside itself and needed no record on their side, that was a JWT. And that card I kept in my pocket and presented each time, that is a cookie. And ek baat sun lo, this exact topic, sessions vs JWT, confuses even developers with years of experience, and it is one of the most asked backend interview questions out there. Most people fumble it. So getting it crystal clear right here genuinely puts you ahead of a lot of them. Today we put all three side by side, and by the end you will know exactly which to use when, and why. Chalo, code is in TypeScript as always.
Quick recall, and the two ways to stay logged in
From the JWT post you know the core problem, HTTP is stateless, the server forgets you after every request. So after you log in once, every next request needs to re-establish who you are.
There are two classic ways to solve this, and this whole post is really about these two.
One, the server keeps a record of you and gives you a claim number. That is a session. AIIMS keeping my file and handing me a card.
Two, the server gives you a self-contained pass that proves everything by itself, and stores nothing. That is a JWT. My Taj key.
And cookies? Cookies are neither of these. Cookies are just the little pouch the browser uses to carry your claim number or pass around. Let us start there, because cookies confuse people the most.
What a cookie actually is
A cookie is a small piece of data that the server tells the browser to store, and which the browser then automatically sends back on every future request to that same site. That is it.
The magic word is automatically. You do not manually attach it each time, the browser does it for you, quietly, on every request. My AIIMS card was almost this, I carried it and presented it each visit, except a browser does the carrying and presenting for you without you lifting a finger.
Here is the key point that clears up all the confusion. A cookie is just a carrier, it is not an authentication method by itself. What you put inside the cookie is the real question. Usually it carries a session id, sometimes it carries a JWT. So it is never really cookies versus JWT, that is comparing a bag to what is inside it.
What a session is: the server remembers you
A session is the stateful approach, meaning the server keeps the state, it remembers you.
Here is the flow. You log in. The server verifies you, then creates a session, a little record of who you are, and stores it on its own side, in memory or a database. It generates a unique session id for that record, and sends that id to the browser inside a cookie. From then on, every request automatically carries that session id cookie, the server takes the id, looks up the matching record in its store, and instantly knows it is you.
That is AIIMS to a T. They made my file (the session), kept it on their side, and gave me a card with a number (the session id in a cookie). Each visit, I showed the card, they looked up my file. The card itself held nothing useful, no medical data, it was just a pointer to the record they were keeping.
What a JWT is: the pass carries everything
You already met this in the last post, but here it is in one line for the comparison. A JWT is the stateless approach. The server gives you a signed, self-contained token that carries your identity inside it, and stores nothing. On each request you send the token, and the server just verifies its signature, no lookup, no record. My Taj key that opened everything by itself.
The heart of it: stateful vs stateless
Strip everything else away and the whole difference is one word.
A session is stateful. The state, the record of who is logged in, lives on the server. AIIMS held my file.
A JWT is stateless. The server holds no state at all, the token carries everything. The Taj held nothing about me, my key did.
Every practical difference between the two flows from this one thing. Let us see those differences, because this is what actually decides which you pick.
Session vs JWT, the real differences
Ab aata hai asli faisla. Here is the honest comparison, the stuff that actually decides which one you pick in a real project, and the exact stuff an interviewer is poking at.
Where the data lives. Session, on the server. JWT, in the token on the client.
Scaling to many servers. This is a big one. With sessions, if your app runs on five servers, they all need to share the session store, otherwise a user logged in on server one is a stranger to server two. So you need a shared store like Redis, which is a fast data store that all your servers can read from together. With JWT, any server can verify the token on its own, no shared store, so scaling is easier. This is the number one reason APIs love JWT.
Revoking access. Sessions win here. To log someone out or ban them, you just delete their record on the server, gone instantly, like AIIMS cancelling my file. With JWT, the token is valid until it expires and the server has no record to delete, so instantly killing one is genuinely hard. That is the trade-off for being stateless.
Size and what is carried. A session id in a cookie is tiny, just a random string. A JWT is bigger because it carries actual data, and it rides on every request.
Where each shines. Sessions grew up with traditional server-rendered websites and cookies. JWT fits APIs, mobile apps, single-page apps, and systems with many services, anywhere the client is not a plain browser or you need easy scaling.
So when do you use which
The practical decision, plainly.
Use sessions when you are building a traditional web app served from your own backend, when you want easy, instant logout and control, and when a shared session store is not a problem. Banks and classic web apps love sessions for exactly this control.
Use JWT when you are building an API that phones, single-page apps, or other services will call, when you need to scale across many servers without a shared store, or when the client is not a simple browser. Most modern APIs go JWT.
And cookies, remember, are not a competing choice, they are the carrier. Sessions almost always use a cookie for the session id. A JWT can be put in a cookie too, or sent in the Authorization header. Bag and contents, that is the relationship.
Building a session in Express with TypeScript
You saw JWT code last post. Here is the session side, using the express-session library, so you can feel the difference in your hands.
npm install express express-session
npm install -D typescript tsx @types/express @types/express-session @types/node
import express, { Request, Response } from "express";
import session from "express-session";
const app = express();
app.use(express.json());
// set up sessions: the server stores the data, the browser gets a cookie with the id
app.use(
session({
secret: "my-session-secret", // signs the session id cookie
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 }, // 1 hour
})
);
// tell TypeScript our session can hold a userId
declare module "express-session" {
interface SessionData {
userId?: number;
}
}
const user = { id: 1, email: "ayush@example.com", password: "1234" };
// LOGIN: verify, then store the user in the session (on the server)
app.post("/login", (req: Request, res: Response) => {
const { email, password } = req.body;
if (email !== user.email || password !== user.password) {
return res.status(401).json({ error: "Wrong email or password" });
}
req.session.userId = user.id; // stored server-side, cookie carries only the id
res.json({ message: "Logged in" });
});
// PROTECTED: the server looks up the session from the cookie the browser sent
app.get("/profile", (req: Request, res: Response) => {
if (!req.session.userId) {
return res.status(401).json({ error: "Not logged in" });
}
res.json({ message: `Welcome back, user ${req.session.userId}` });
});
// LOGOUT: destroy the server-side session, instantly revoked
app.post("/logout", (req: Request, res: Response) => {
req.session.destroy(() => res.json({ message: "Logged out" }));
});
app.listen(3000, () => console.log("Server on http://localhost:3000"));
Read the difference from the JWT code. Here, req.session.userId = user.id stores your identity on the server, and express-session automatically sends the browser a cookie holding just the session id. On /profile, the server reads that cookie, looks up your session, and finds your userId, that is the AIIMS counter pulling my file by the card number. And /logout calls req.session.destroy, wiping the record on the server instantly, the easy revocation JWT cannot match. Notice there is no token carrying data here, the cookie holds only a pointer, the real stuff lives on the server.
Try it yourself
Do not just read it, khud karke dekho. Install the packages, put the code in index.ts, run npx tsx index.ts. Because sessions live in a cookie, use curl with a cookie jar so it stores and resends the cookie like a browser would.
# log in, and save the cookie to a file (like the browser keeping it)
curl -c cookies.txt -X POST http://localhost:3000/login -H "Content-Type: application/json" -d '{"email":"ayush@example.com","password":"1234"}'
# visit the protected route WITH the saved cookie, you are recognised
curl -b cookies.txt http://localhost:3000/profile
# visit it WITHOUT the cookie, you are a stranger
curl http://localhost:3000/profile
# log out, which destroys the session on the server
curl -b cookies.txt -X POST http://localhost:3000/logout
Watch it. Login sets a cookie. Calling /profile with the cookie recognises you, without it you get a 401. After logout, even the same cookie no longer works, because the record it pointed to is gone from the server. That is the card working only while AIIMS keeps your file.
The mistakes that will trip you up
The traps around this topic, most of them are conceptual.
Thinking it is cookies vs JWT. It is not. Cookies are the carrier, sessions and JWT are the strategies. A JWT can live inside a cookie. Do not compare a bag with its contents.
Storing sessions in memory in production. The default express-session store keeps sessions in the server's memory, so a restart logs everyone out, and multiple servers do not share them. Real apps use a shared store like Redis.
Not securing the cookie. A cookie carrying a session id or token must be protected. Set httpOnly so JavaScript cannot read it, secure so it only travels over HTTPS, and sameSite to limit cross-site sending. An unprotected auth cookie is a real risk.
Expecting to revoke a JWT like a session. You cannot instantly kill a plain JWT, it stays valid till expiry. If instant logout and banning matter a lot, that is a point for sessions, or you add extra machinery to JWT.
Picking one because it is trendy. JWT is popular, but a classic server-rendered web app is often simpler and safer with sessions. Choose by your actual needs, not the hype.
Quick reference, bookmark this bit
The whole post in a thirty-second scan.
A cookie is a carrier the browser stores and auto-sends. A session keeps your data on the server and puts only an id in the cookie (stateful). A JWT keeps your data inside the token and the server stores nothing (stateless).
| Session (stateful) | JWT (stateless) | |
|---|---|---|
| State lives | On the server | Inside the token |
| Cookie holds | A session id (a pointer) | The token itself, or use a header |
| Scaling | Needs a shared store (Redis) | Any server verifies alone |
| Logout / revoke | Easy, delete the record | Hard, valid till expiry |
| Size on each request | Tiny id | Bigger token |
| Best for | Traditional web apps | APIs, mobile, SPAs |
The rules worth memorizing. Cookies carry, sessions and JWT are the two strategies. Session equals server remembers you, an id in a cookie points to the record, easy to revoke, needs a shared store to scale. JWT equals server remembers nothing, the token carries everything, scales freely, hard to revoke. Traditional web app, lean sessions. API or mobile or many services, lean JWT. And always secure your auth cookie with httpOnly, secure, and sameSite.
Wrapping up
So that is the whole picture. Cookies are just the pouch the browser carries things in, automatically, on every request. A session is the stateful way, the server keeps your record and the cookie holds only a claim id, which makes logout easy but scaling need a shared store. A JWT is the stateless way, the token carries your identity itself and the server stores nothing, which makes scaling easy but instant logout hard. Cookies versus JWT was never the real question, the real question is sessions versus JWT, and cookies just carry whichever one you chose.
That was AIIMS versus the Taj, really. AIIMS kept my file and gave me a card that pointed to it, a session. The Taj gave me a key that carried everything itself, a JWT. Both got me recognised without proving myself again and again, they just remembered me in opposite ways. Pick the one whose trade-off fits your app, and let the cookie do the carrying.
Next up, notice that all through this series our data has been living in little in-memory arrays that vanish the moment the server restarts. It is finally time to store it for real, permanently. That is my next post, Modern Database Access, Prisma, Drizzle, and ORMs Explained.
I hope you enjoyed reading this.



