JWT vs Sessions: What I Learned Building Apps
⚡ TL;DR - Quick Decision Guide
Building a traditional web app? → Use
Sessions
Building an API or React app? → Use
JWT
Not sure? → Read the 3-question test
below
Stop overthinking authentication. You have two solid choices: JWT or Sessions. Both work. Both are secure when done right. The difference? Sessions are like a hotel key card, JWT is like a driver's license. Let me show you which one fits your project.
What you'll get from this guide:
- A 3-question framework to choose the right method
- Real code examples (not just theory)
- Common mistakes that'll save you hours of debugging
- Security best practices that actually matter
Sessions = Hotel Key Card System
How it works: Server stores your data, gives you an ID to reference it.
💡 Think of it like...
Checking into a hotel. You show ID at front desk → They give you room key → They keep your details on file → You show key for everything
Sessions Code Example
// Session-based login - server remembers everything
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Check if user exists and password is correct
const user = await User.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.hashedPassword)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create session - server stores user info
req.session.userId = user.id;
req.session.role = user.role;
res.json({ message: 'Login successful' });
});
JWT = VIP Pass System
How it works: All your info is encoded in the token itself. No server storage needed.
💡 Think of it like...
A VIP concert pass. All your access info is printed on the pass → Security scans it → No need to check a database
JWT Code Example
// JWT-based login - client stores everything
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Same validation as before
const user = await User.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.hashedPassword)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create JWT token with user info inside
const token = jwt.sign(
{
userId: user.id,
role: user.role,
// Role could be 'admin', 'user', 'moderator', etc.
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
token,
user: { id: user.id, email: user.email }
});
});
Key Difference
Sessions: "Here's your room number
(123), we'll keep your details at the front desk"
JWT: "Here's your driver's license
with all your info on it"
Sessions vs JWT: Quick Comparison
Sessions Win When...
- Security first - Banking, medical apps
- Instant logout needed - Admin panels
- Traditional web app - Forms, server pages
- Small scale - Under 1K users
JWT Wins When...
- APIs & SPAs - React, Vue, Angular
- Mobile apps - Native iOS/Android
- Microservices - Multiple backends
- Need to scale - 1K+ concurrent users
Aspect | Sessions | JWT |
---|---|---|
Storage Location | Server-side (memory, database, Redis) | Client-side (localStorage, cookies) |
Scalability | Requires shared storage or sticky sessions | Stateless, easily scalable |
Security | Server controls all session data | Vulnerable to XSS if stored in localStorage |
Performance | Database/Redis lookup per request | No server-side lookup needed |
Logout/Revocation | Immediate session termination | Token valid until expiration |
Mobile Apps | Cookie handling complexities | Easy to implement |
Microservices | Shared session store required | Self-contained, service-independent |
Real Scenarios: When to Use What
I Reach for Sessions When...
Sessions work great for traditional web applications. Here's when I choose them:
- Building a classic web app - Forms, server-rendered pages, the usual stuff
- Security is critical - Banking apps, admin panels where instant logout matters
- Simple architecture - One server, straightforward setup
- You want full control - Need to track active users, manage permissions dynamically
Pro tip: If you're building something like a content management system or e-commerce admin panel, sessions are usually the way to go.
// Basic session setup - works great for most web apps
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 8 * 60 * 60 * 1000 // 8 hours - reasonable for most apps
}
}));
I Go with JWT When...
JWT shines in these scenarios:
- Building an API - REST APIs, GraphQL endpoints, anything mobile apps will consume
- React/Vue/Angular apps - SPAs that need to manage auth state on the frontend
- Multiple services - Microservices that all need to verify the same users
- Need to scale - When you expect lots of concurrent users
Real example: For my claim management app, JWT was perfect because I had patients and insurers using different interfaces, but the same backend API needed to handle both roles seamlessly.
// JWT middleware - clean and stateless
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user; // Now you have access to user info in all routes
next();
});
};
"Here's the truth: both approaches work well when implemented correctly. The 'best' choice depends entirely on what you're building and how it needs to work."
The Modern Approach: Best of Both Worlds
Here's a pattern I've started using more often - refresh tokens. It's like having both a temporary pass and a permanent ID:
Refresh Token Strategy
// Modern approach: short-lived access token + long-lived refresh token
app.post('/login', async (req, res) => {
// ... validate credentials ...
// Short-lived access token (15 minutes)
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// Long-lived refresh token (7 days)
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token so we can revoke it if needed
await RefreshToken.create({
token: refreshToken,
userId: user.id
});
res.json({ accessToken, refreshToken });
});
Why this works: Users stay logged in for a week (good UX), but if someone steals their access token, it only works for 15 minutes (good security).
Common Mistakes (So You Don't Make Them)
Security Gotchas I've Learned
- Don't store JWT in localStorage - Use httpOnly cookies when possible
- Always use HTTPS in production - No exceptions
- Keep JWT payloads small - Only essential info
- Set reasonable expiration times - Balance security and UX
Performance Lessons
- Use Redis for sessions - In-memory is much faster than database
- Index your user lookup fields - Speed up authentication queries
- Consider connection pooling - Reuse database connections
- Consider token refresh mechanisms
Quick Decision Framework
Answer These 3 Questions:
1. What are you building?
Traditional web app?
→
Use Sessions
API or React app?
→ Use
JWT
2. How many users?
Under 1K users?
→ Either
works
1K+ users?
→ JWT scales
easier
3. How sensitive?
Banking/Medical?
→
Sessions safer
Standard app?
→ JWT is
fine
My advice: Start simple, ship fast, and iterate based on real user feedback. That's how you build things people actually want to use.