Skip to Content
Back to Blog
Backend DevelopmentAugust 15, 2025

JWT vs Sessions: What I Learned Building Apps

Confused about JWT vs sessions? Software Engineer Kripanshu Singh explains both authentication methods with practical examples, code samples, and a quick decision framework. Learn which one to choose for your next project.

Cover for 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. (If you’re also wondering about which runtime to build your auth server on, check out my Node.js vs Bun vs Deno comparison.)

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: The server stores your session data and gives the client an ID to reference it.

💡 Think of it like…

Checking into a hotel. You show your ID at the front desk → they give you a room key card → they keep your details on file → you show the key card to access 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 of the user information is encoded directly in the token itself. No server-side session storage is needed.

💡 Think of it like…

A VIP concert pass. All of your access info is printed directly on the pass → security scans it and verifies it on the spot → 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,
    },
    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 printed on it.”

Sessions vs JWT: Quick Comparison

Sessions Win When…

  • Security first: Perfect for banking or medical applications.
  • Instant revocation/logout needed: Important for admin panels.
  • Traditional web apps: Built with server-side rendered pages and forms.
  • Small scale: Simplifies hosting for under 1,000 active users.

JWT Wins When…

  • APIs & SPAs: Essential for React, Vue, Angular, or Next.js apps.
  • Mobile apps: Works natively with iOS and Android storage.
  • Microservices: Eliminates the need for a shared session database.
  • Large scale: Stateless nature easily handles thousands of concurrent requests.
Aspect Sessions JWT
Storage Location Server-side (Memory, Redis, DB) Client-side (Cookies, LocalStorage)
Scalability Needs shared storage / sticky sessions Stateless, scales infinitely
Security Server controls all session data Vulnerable to XSS if not stored securely
Performance Database/Redis lookup per request Cryptographic check (no database hit)
Logout/Revocation Immediate session termination Harder (valid until token expires)
Mobile Apps Cookie handling complexities Native header storage is easy
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. If you are looking to secure a traditional web application like the CineVault Media Catalog or the Lost & Found Campus Mobile App, cookie-based sessions are often the most secure and straightforward option. Here’s when I choose them:

  • Building a classic web app: Standard forms, server-rendered layouts, and classic page refreshes.
  • Security is critical: Financial platforms or administrative backends where you must be able to instantly revoke access.
  • Simple architecture: Single-server monoliths where shared state isn’t a bottleneck.
  • Full control: When you need to monitor active logins and update permissions dynamically.
// 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
  }
}));

I Go with JWT When…

JWT shines in these scenarios. For example, when building the Claim Management Portal and the QuickDoc Healthcare Suite, I implemented JWT authentication to securely handle multi-tenant medical roles across distributed API endpoints. Here’s when I choose them:

  • Building an API: REST or GraphQL backends consumed by mobile apps or third-party integrations.
  • Single Page Apps (SPAs): Frontend-heavy apps that communicate purely via API endpoints.
  • Multiple services: Distributed microservices that all need to verify requests independently.
  • Need to scale: High concurrent traffic where database access for sessions is a performance bottleneck.
// 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;
    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

A highly effective pattern is using short-lived access tokens combined with long-lived refresh tokens:

// 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 in database to allow revocation if needed
  await RefreshToken.create({ 
    token: refreshToken, 
    userId: user.id 
  });
  
  res.json({ accessToken, refreshToken });
});

Why this works: Users stay logged in for a week (great UX), but if someone steals their access token, it only works for 15 minutes (great security).


Common Mistakes (So You Don’t Make Them)

Security Gotchas

  • Don’t store JWT in LocalStorage: It is highly vulnerable to Cross-Site Scripting (XSS). Use httpOnly secure cookies.
  • Always use HTTPS in production: Unencrypted tokens can be easily intercepted.
  • Keep JWT payloads small: Every byte in the token is sent on every single HTTP request.
  • Set reasonable expiration times: Never set infinite token lifetimes.

Performance Lessons

  • Use Redis for sessions: Standard database lookups per request will slow your app down.
  • Index your user lookup fields: Ensure database queries for auth are optimized.
  • Consider connection pooling: Avoid exhausting database connections on auth checks.

Quick Decision Framework

1. What are you building?

  • Traditional web app? → Use Sessions
  • API or React/Vue app? → Use JWT

2. How many active users?

  • Under 1,000 users? → Either works. Choose the easiest one.
  • 1,000+ users? → JWT makes scaling easier.

3. How sensitive is the data?

  • Banking/Medical? → Sessions offer safer, instant revocation control.
  • Standard application? → JWT works perfectly.

My advice: Start simple, ship fast, and iterate based on real feedback. That’s how you build things people actually want to use.