October 17, 2026
Sessions vs JWT: The Day Authentication Finally Made Sense to Me
For the longest time, authentication felt like one of those backend topics everyone pretends to understand.

By Arpit Srivastav
6 min read
I could wire up a login flow. I could generate JWT tokens. I knew sessions existed. I had even used Redis for session storage.
But if someone had asked me:
"What exactly is the difference between Session and JWT at an architectural level?"
my answer would probably have been:
"JWT is stateless and Sessions are stateful."
Which, honestly, explains almost nothing.
Everything finally clicked for me while thinking about a very simple question:
"After login, how does the server remember who I am?"
That question is the entire story of authentication.
It All Starts With HTTP Being Stateless
Imagine opening Instagram.
You log in once in the morning.
Then throughout the day you:
- open your profile
- scroll reels
- like posts
- send messages
- comment on memes
From a user perspective, it feels like one continuous session.
But technically?
Every click is a completely new HTTP request.
The request to:
GET /profileGET /profilehas absolutely no connection with:
POST /likePOST /likeHTTP does not remember anything automatically.
The server doesn't magically know:
- who you are
- whether you logged in
- whether you are admin
- whether your session is valid
unless identity information is sent with every request.
And that is the entire purpose of authentication systems.
The Coffee Shop Analogy
Think of a coffee shop with prepaid memberships.
The first time you walk in:
- they verify your identity
- confirm your membership
- and hand you a membership card
Now every time you come back: you simply show the card.
The interesting part is: there are two fundamentally different ways this card system can work.
And those two approaches are basically:
- Sessions
- JWT
Sessions: "The Shop Remembers You"
Sessions are the older and more traditional approach.
Here's how they work.
You log in:
POST /login
{
"email": "user@example.com",
"password": "1234"
}POST /login
{
"email": "user@example.com",
"password": "1234"
}The backend verifies your password.
Then instead of sending your actual identity back and forth repeatedly, the server creates something like:
SESSION_ID = ABC123SESSION_ID = ABC123Now here's the important part.
The server stores:
ABC123 -> Arpit's accountABC123 -> Arpit's accountinside:
- memory
- Redis
- database
The browser receives:
Set-Cookie: SESSION_ID=ABC123Set-Cookie: SESSION_ID=ABC123And from that moment onward, every request automatically contains:
Cookie: SESSION_ID=ABC123Cookie: SESSION_ID=ABC123The server receives the ID and checks:
ABC123 -> UserId 55ABC123 -> UserId 55Authentication complete.
The Mistake Most Beginners Make
People often think Sessions and Cookies are the same thing.
They are not.
The cookie is only the transport mechanism.
The actual session lives on the server.
A cookie might contain:
SESSION_ID=ABC123SESSION_ID=ABC123But the real authentication state exists in backend storage.
That distinction matters a lot later.
Why Sessions Feel So Natural
Sessions are conceptually simple because the server is fully in control.
Want logout?
Delete the session.
Want to invalidate user access immediately?
Delete the session.
Want to track active devices?
Easy.
The server owns the authentication state completely.
And honestly, for monolith applications, sessions work beautifully.
But then systems became distributed.
And things got complicated.
The Scaling Problem Nobody Notices Initially
Imagine your application grows.
Now you have:
- 10 backend servers
- load balancers
- microservices
- Kubernetes
- autoscaling
User logs into:
Server AServer ASession exists there.
Next request goes to:
Server BServer BServer B has no idea who this user is.
Now you suddenly need:
- sticky sessions
- shared Redis session store
- distributed synchronization
Your authentication system becomes infrastructure-heavy.
And this is exactly where JWT entered the industry.
JWT Changed One Fundamental Thing
JWT flipped the model completely.
Instead of:
"Server remembers the user"
JWT says:
"The client carries proof of identity."
That's the core idea.
JWT Login Flow
User logs in normally.
Backend validates credentials.
But instead of creating a server-side session, it creates a signed token.
Example payload:
{
"userId": 55,
"role": "ADMIN",
"exp": 1719999999
}{
"userId": 55,
"role": "ADMIN",
"exp": 1719999999
}The client stores this token.
Future requests contain:
Authorization: Bearer eyJhbGc...Authorization: Bearer eyJhbGc...Now the server does NOT need Redis lookup.
It simply:
- verifies signature
- checks expiry
- extracts claims
Authentication done.
No centralized session state.
The First Time JWT Really Clicked for Me
I used to think JWT somehow removed requests between client and server.
It doesn't.
Requests still happen exactly the same way.
The only thing that changes is:
how the server verifies identity.
With Sessions:
Client sends session ID
โ Server fetches identity from RedisClient sends session ID
โ Server fetches identity from RedisWith JWT:
Client sends signed identity proof
โ Server validates cryptographicallyClient sends signed identity proof
โ Server validates cryptographicallyThat's it.
And suddenly the "stateless" buzzword finally made sense.
JWT Structure Looks Fancy But Is Surprisingly Simple
A JWT has 3 parts:
HEADER.PAYLOAD.SIGNATUREHEADER.PAYLOAD.SIGNATUREExample:
abc.xyz.pqrabc.xyz.pqrHeader
Contains metadata.
Example:
{
"alg": "HS256",
"typ": "JWT"
}{
"alg": "HS256",
"typ": "JWT"
}Mostly:
- algorithm
- token type
Payload
Contains claims.
Claims are simply user information.
Example:
{
"userId": 55,
"role": "ADMIN"
}{
"userId": 55,
"role": "ADMIN"
}And here's something very important:
JWT payload is NOT encrypted.
Anyone can decode it. (Encrypted JWTs, called JWE, do exist, but most systems use plain signed ones.)
Never store:
- passwords
- secrets
- bank details
inside JWT payload.
One more thing: claims are a snapshot from login time. If you demote a user from ADMIN to USER, the token they already hold still says ADMIN until it expires. Short expiry times are what keep that window small.
Signature
This is where security actually comes from.
Without signature, anyone could modify:
"role": "USER""role": "USER"to:
"role": "ADMIN""role": "ADMIN"The signature prevents tampering.
The server signs the token with a key.
When requests arrive: the server verifies the signature.
If anything changed: token becomes invalid.
One detail that's easy to miss: with HS256 (a shared secret), verifying means recalculating the signature and comparing it. With RS256 or ES256, the auth server signs with a private key and every other service verifies with the public key, so services can check tokens without ever holding the signing key.
Also, always fix the expected algorithm on the server. Never trust whatever the token's own alg header says.
So Why Did Companies Move Toward JWT?
One word:
ScalabilityScalabilityWith Sessions:
Every request may require:
API Server โ Redis lookupAPI Server โ Redis lookupA single Redis lookup is fast (usually well under a millisecond). The real cost is that every service now depends on one shared store being up and reachable.
With JWT:
API Server validates token locallyAPI Server validates token locallyNo centralized auth dependency.
Perfect for:
- microservices
- distributed systems
- APIs
- mobile apps
This is why JWT became so popular in distributed architectures.
But JWT Introduced New Problems Too
This part is important because JWT is often overhyped.
Sessions had one beautiful property:
Logout was easy.
Delete session. Done.
JWT broke that simplicity.
Because JWT is stateless.
If a token is valid for 30 minutes: it remains valid until expiry.
Even after logout.
Picture this: an employee's laptop is stolen, and security asks you to log them out of everything right now. With sessions, that's one delete. With a JWT that's valid for 24 hours, the token keeps working until it expires, and the server has nothing to delete.
That's why modern systems introduced:
- refresh tokens
- token rotation
- Redis denylists (which means a lookup on every request again)
- short-lived access tokens
Ironically, many "stateless" architectures slowly reintroduced state again.
Here's how the refresh token part usually works:
โ The access token is short-lived (5 to 15 minutes) and verified locally.
โ The refresh token is long-lived, stored server-side, and only ever sent to the auth endpoint.
โ Every time it's used, the server issues a new refresh token and invalidates the old one. This is called refresh token rotation.
โ If an old refresh token shows up again, that's a sign it was stolen, so the server revokes the whole token family and forces a fresh login.
So logout and stolen-device cases are handled by revoking the refresh token. The worst case is a few minutes until the current access token expires.
The Security Mistake Almost Everyone Makes Initially
Storing JWT in localStorage.
Looks convenient.
Terrible idea.
If your application gets hit by XSS: attacker steals the token instantly.
Modern systems usually prefer:
- HTTP-only cookies
- Secure cookies
- SameSite protection
because browser JavaScript cannot directly access them.
One catch: the browser sends cookies automatically, which brings back CSRF risk. If your JWT lives in a cookie, SameSite plus a CSRF token (or an Origin header check) needs to be part of the design.
For refresh tokens, scoping the cookie to the refresh endpoint (for example Path=/auth/refresh) also keeps it from being sent on every request.
So Which One Is Better?
This is the wrong question.
The real question is:
"Better for what kind of system?"
Sessions Are Excellent For
- monolith applications
- admin dashboards
- traditional server-rendered apps
- systems prioritizing simplicity
Why?
Because:
- logout is simple
- revocation is simple
- debugging is easier
- security model is straightforward
JWT Is Excellent For
- microservices
- mobile apps
- distributed systems
- third-party APIs
- cloud-native architectures
Why?
Because:
- no centralized auth storage
- independent validation
- scalable horizontally
- easier cross-service communication
What Many Production Systems Actually Use
This was another thing that surprised me.
Many production systems don't use "pure JWT".
They use hybrid models.
Something like:
Short-lived JWT access token
+
Refresh token
+
Redis-backed revocation/session layerShort-lived JWT access token
+
Refresh token
+
Redis-backed revocation/session layerBecause real engineering is always about trade-offs.
Not ideology.
The Mental Model That Finally Made Everything Click
After struggling with authentication concepts for a long time, this became my simplest understanding:
Session
Client carries reference
Server carries identityClient carries reference
Server carries identityJWT
Client carries signed identity proof
Server validates cryptographicallyClient carries signed identity proof
Server validates cryptographicallyAnd honestly, once you truly understand this distinction, authentication systems stop feeling magical.
They start feeling like architecture decisions.
If You Enjoyed This Story
If you found this article useful, a like or share would mean a lot! โค๏ธ
I'd love to hear your thoughts, questions, or experiences with building resilient, large-scale systems โ drop them in the comments below!
Got a topic in mind โ whether it's replication, failover, database architecture, event-driven architecture, scaling or another engineering challenge?
Let me know, and I'll dive into it in my next story. ๐