When you build a login system, you'll quickly run into three terms:
Cookies, sessions, and JWTs.
They are often mentioned together, which makes it easy to assume that they are three different ways of doing the exact same thing.
They're not.
A cookie is mainly a way for a browser to store and send data.
A session is server-side state that can be used to remember a logged-in user.
A JWT is a signed token that can carry information about an authenticated user.
The confusion usually starts because these things can also be used together.
For example:
Browser
↓
Cookie
↓
Session ID
↓
Server-side Session
Or:
Browser
↓
Cookie
↓
JWT
So instead of asking:
"Which one is better?"
it's more useful to understand what each one actually does and then decide which combination fits your application.
First, What Problem Are We Solving?
Let's start with a normal login.
A user sends:
POST /api/login
with:
{
"email": "alex@example.com",
"password": "mypassword"
}
The backend verifies the credentials.
Let's say they are correct.
The server now knows:
User ID: 42
But what happens when the user makes another request?
GET /api/profile
HTTP doesn't automatically remember that the previous request was a successful login.
The application needs a way to recognize the user again.
That's the problem these authentication mechanisms help solve.
Cookies: How Does the Browser Remember Something?
Let's start with cookies because they are the easiest to misunderstand.
A cookie is not an authentication system by itself.
It's a small piece of data that a browser can store and send back to a website.
A server can send:
Set-Cookie: session_id=abc123
The browser stores it.
Later, when making another request to the relevant site, the browser can send:
Cookie: session_id=abc123
That's the basic idea.
You can think of a cookie as a container for a value that the browser knows how to send back to the server.
That value could be:
session_id
authentication-related data
preferences
language settings
analytics identifiers
So:
Cookie ≠ Session
Cookie ≠ JWT
A cookie is a transport/storage mechanism.
What you put in it determines how it's being used.
Sessions: Keep the Login State on the Server
Now let's look at sessions.
After a successful login, the server can create a session:
Session ID: abc123
User ID: 42
The actual session information stays on the server.
Conceptually:
abc123 → User 42
The browser only needs to keep the session identifier.
That's where cookies often come in.
The server sends:
Set-Cookie: session_id=abc123
The browser stores it.
On a later request:
GET /api/profile
Cookie: session_id=abc123
the server reads the session ID and looks up the corresponding session.
The flow is:
Login
↓
Verify credentials
↓
Create session
↓
Store session on server
↓
Send session ID in cookie
↓
Browser stores cookie
↓
Browser sends cookie later
↓
Server finds session
↓
User identified
This is why you will often see cookies and sessions used together.
The cookie carries the identifier.
The session holds the state.
A Simple Example
Imagine a server has this session store:
abc123 → User 42
xyz789 → User 91
Alex logs in and receives:
session_id=abc123
When Alex requests:
GET /api/profile
Cookie: session_id=abc123
the server can do:
abc123
↓
Session Store
↓
User 42
↓
Fetch profile
The browser never needs to know what is stored inside the server-side session.
That's one of the useful properties of this approach.
So Where Does JWT Come In?
JWT stands for JSON Web Token.
Instead of giving the client a random session ID that points to server-side authentication state, the server can issue a signed token containing claims.
A simplified payload might look like:
{
"sub": "42",
"role": "user",
"exp": 1760003600
}
The server signs the token.
The resulting JWT looks roughly like:
Header.Payload.Signature
The client can then send it with future API requests.
A common format is:
Authorization: Bearer eyJ...
The backend receives the token and verifies its signature and relevant claims.
Conceptually:
Login
↓
Verify credentials
↓
Create JWT
↓
Sign JWT
↓
Send JWT to client
↓
Client sends JWT
↓
Server verifies JWT
↓
Identify user
That's the basic JWT model.
The Important Difference
At this point, the three terms should start looking different.
| What it does | |
|---|---|
| Cookie | Stores and sends data through the browser |
| Session | Keeps authentication state on the server |
| JWT | Carries signed claims that a server can verify |
This is why comparing them as if they were direct alternatives can be confusing.
They're not really at the same layer.
For example, you can have:
Cookie + Session
or:
Cookie + JWT
or:
Authorization Header + JWT
Those are all different architectures.
Cookie + Session
This is a common pattern for traditional web applications.
The architecture looks like:
Browser
|
Cookie
|
Session ID
|
↓
Server-side Session
|
User ID
For example:
Cookie:
session_id=abc123
Server:
abc123 → User 42
The browser carries the identifier.
The server owns the actual session state.
One practical advantage is that the server can invalidate the session directly.
For example:
Logout
↓
Invalidate session
↓
Session ID no longer works
This can make server-side session management convenient.
Cookie + JWT
A JWT doesn't have to be stored in JavaScript-accessible browser storage.
It can also be placed in a cookie.
For example:
Set-Cookie: access_token=eyJ...; HttpOnly; Secure
The browser then sends the cookie with appropriate requests.
The architecture becomes:
Browser
↓
Cookie
↓
JWT
↓
Server verifies JWT
Here the cookie is handling browser storage/transmission while the JWT is the authentication credential.
Again:
Cookie ≠ JWT
They are doing different jobs.
JWT in the Authorization Header
Another common pattern is to send the JWT explicitly:
GET /api/profile
Authorization: Bearer eyJ...
This is common when working with APIs where the client is responsible for attaching the token.
The server receives it and performs verification.
Request
↓
Authorization header
↓
JWT
↓
Verify token
↓
Identify user
↓
Process request
This pattern is common in applications where the frontend, mobile app, or another client communicates with a backend API.
But having an API does not automatically mean JWT is required.
That's an important distinction.
What Does "Stateless JWT" Mean?
One reason JWTs are popular is that they can support a stateless authentication model.
With a traditional session:
Session ID
↓
Server
↓
Session Store
↓
User
The server needs access to the session state.
With a self-contained JWT:
JWT
↓
Verify signature
↓
Read claims
↓
Identify subject
The server can potentially authenticate the request without looking up a traditional session record.
This can be useful when an application has multiple backend servers:
Load Balancer
/ | \
/ | \
Server A Server B Server C
\ | /
\ | /
Verify JWT
Each server can verify the token using the appropriate key.
However, calling JWT authentication "stateless" doesn't mean the entire authentication system has zero state.
Applications may still need state for:
Refresh tokens
Revocation
Device sessions
Security events
User sessions
So stateless JWT authentication is a design choice, not a guarantee that the whole system becomes state-free.
What About Security?
This is where the choice becomes more interesting.
For browser applications, cookies have security-related attributes such as:
HttpOnly
Secure
SameSite
For example:
Set-Cookie: session_id=abc123;
HttpOnly;
Secure;
SameSite=Lax
HttpOnly prevents ordinary JavaScript from reading the cookie directly.
Secure restricts the cookie to HTTPS connections.
SameSite controls when the browser sends the cookie in cross-site situations.
These options are useful, but they don't magically make authentication secure.
The application still needs to consider things like:
HTTPS
CSRF
XSS
Token theft
Session fixation
Password security
Session expiration
Authorization
The correct security setup depends on the application.
What About localStorage?
You may have seen JWTs stored like this:
localStorage.setItem("token", token);
It is convenient.
But there's an important trade-off.
JavaScript running on the page can read values stored in localStorage.
So if an attacker manages to execute malicious JavaScript through an XSS vulnerability, a token stored there may be accessible to that script.
This doesn't mean:
localStorage = always insecure
or:
cookies = automatically secure
Security is more complicated than that.
It means you should understand what each storage mechanism exposes and choose based on your application's threat model.
What Happens During Logout?
This is one of the places where sessions and JWTs behave differently.
With a server-side session:
Logout
↓
Invalidate session
↓
Session ID becomes useless
The server can immediately stop recognizing that session.
With a self-contained access JWT, the server may continue accepting the token until it expires unless the application has an additional revocation mechanism.
That's one reason JWT-based systems often use short-lived access tokens.
For example:
Access token
↓
15-minute lifetime
A refresh mechanism can then be used to obtain another access token when necessary.
A common flow is:
Login
↓
Access Token + Refresh Token
↓
Use Access Token
↓
Access Token expires
↓
Use Refresh Token
↓
Get new Access Token
Refresh-token handling needs its own security design.
Scaling Sessions
Now imagine your application grows.
Initially:
Users
↓
Server
↓
Database
Later:
Load Balancer
↓
┌───────┼───────┐
↓ ↓ ↓
Server A Server B Server C
With server-side sessions, you need to decide where those sessions live.
For example:
Server A ──┐
Server B ──┼──→ Shared Session Store
Server C ──┘
A shared store allows different servers to find the same session.
This adds infrastructure, but it is a perfectly reasonable architecture.
With JWTs, each server can potentially validate the token independently:
Server A → Verify JWT
Server B → Verify JWT
Server C → Verify JWT
That can simplify some scaling scenarios.
But it doesn't eliminate the other problems involved in distributed authentication.
You still have to think about:
Key management
Token expiration
Refresh tokens
Revocation
Authorization
So Which One Should You Use?
This is the part where generic tutorials often go wrong.
They say:
"Use JWT because it's modern."
That's not a useful architectural decision.
Start with the application.
A traditional web application
If most of your application is server-rendered and the server already owns the user's session state, a session-based approach can be a straightforward choice:
Browser
↓
Secure Cookie
↓
Session ID
↓
Server Session
A frontend + backend API
If you have separate clients communicating with an API, token-based authentication may fit naturally:
Web App
Mobile App
Desktop App
↓
API
↓
Authentication
JWT can be useful here, but it isn't mandatory.
Cookies and sessions can also be used with APIs.
A distributed system
If several services need to verify the same authentication information, signed tokens can be convenient:
API Gateway
↓
┌──────────┼──────────┐
↓ ↓ ↓
Service A Service B Service C
↓ ↓ ↓
└──── Verify identity ──┘
Again, the architecture determines whether that benefit actually matters.
The Questions I Would Ask Before Choosing
Instead of starting with:
"Should I use JWT?"
I'd ask:
Who are the clients?
Is the application browser-based?
Do we need server-side session state?
How should logout work?
Do we need immediate credential revocation?
How long should authentication credentials live?
How will refresh work?
Where will credentials be stored?
Do multiple services need to verify the identity?
What threats does the application need to handle?
These questions tell you much more than simply choosing a technology because it's popular.
The Mental Model
If you remember only one thing from this article, remember this:
Cookie
→ How the browser stores and sends a value
Session
→ Authentication state maintained by the server
JWT
→ A signed token containing claims
And they can be combined:
Cookie + Session
or:
Cookie + JWT
or:
Authorization Header + JWT
They're not mutually exclusive.
That's the part that usually clears up the confusion.
Final Takeaway
Cookies, sessions, and JWTs are often discussed together, but they solve different problems.
A cookie is a mechanism for storing and sending data through the browser.
A session is server-side state that can be used to remember an authenticated user.
A JWT is a signed token that carries claims which a server can verify.
The right architecture depends on what you're building.
A small server-rendered application might be perfectly comfortable with cookies and server-side sessions.
A system serving multiple types of clients might benefit from token-based authentication.
A distributed system might find signed tokens useful for passing verifiable identity information between services.
None of those choices makes one technology universally better than the others.
The better question isn't:
"JWT or sessions?"
It's:
"Where should authentication state live, how should the client carry it, and how should the server verify and revoke it?"
Once you start thinking in those terms, cookies, sessions, and JWTs stop looking like competing technologies and start looking like different pieces of an authentication architecture.
Top comments (0)