10 min read3 hours ago
–
Press enter or click to view image in full size
Every developer who has ever tried to implement authentication eventually faces the same question: JWT or Sessions?
It’s a simple question on the surface, and yet, it sparks some of the most opinionated debates in web development. Entire threads on Hacker News and Reddit have been dedicated to proving why one approach is “more modern” or “more secure” than the other. It’s almost become a religion: the stateless believers vs the session traditionalists.
When I started building my NestJS Auth Kit, I thought I already knew the answer. I leaned toward JWTs — they were faster, more scalable, and everyone seemed to be using them. But as I implemented features like logout, refresh tokens, and multi-device sessions…
10 min read3 hours ago
–
Press enter or click to view image in full size
Every developer who has ever tried to implement authentication eventually faces the same question: JWT or Sessions?
It’s a simple question on the surface, and yet, it sparks some of the most opinionated debates in web development. Entire threads on Hacker News and Reddit have been dedicated to proving why one approach is “more modern” or “more secure” than the other. It’s almost become a religion: the stateless believers vs the session traditionalists.
When I started building my NestJS Auth Kit, I thought I already knew the answer. I leaned toward JWTs — they were faster, more scalable, and everyone seemed to be using them. But as I implemented features like logout, refresh tokens, and multi-device sessions, I kept running into contradictions. The more “stateless” I tried to go, the more “state” I had to reintroduce.
That was when I realized something: the industry doesn’t actually pick sides. Systems at scale — from Google to GitHub to Stripe — quietly blend both approaches.
After breaking both models in real-world tests, I learned that authentication isn’t and shouldn’t be a binary choice between “stateless” and “stateful.” It should be about finding the point where performance meets control, and where simplicity meets security.
In this article, I will explain why each of them has its benefits, and why a hybrid approach is the best to building a scalable authentication system.
2. Session-Based Authentication — The Old Reliable
Before JWTs took over the conversation, sessions were the backbone of web authentication. They’re the old reliable — simple, predictable, and battle-tested. Session token authentication functions by tracking the state of a signed-in user on the server using the concept of a “session”.
Here’s how they work at a conceptual level:
Press enter or click to view image in full size
When a user logs in, the server creates a record, a session, that represents that user’s authenticated state. It stores this record somewhere safe (in memory, Redis, or a database), and gives the user a session ID stored in a cookie.
On every request, the browser automatically sends that cookie back to the server, which looks up the session ID, verifies it, and restores the user’s identity.
It’s like running a guest list at the door: if your name’s on the list, you’re allowed in. The bouncer (the server) doesn’t need to inspect your entire life story every time — it just checks if you’re still on the list.
Why Sessions Still Work So Well
Sessions shine because they rely on server-side trust. The server owns the source of truth, so it can instantly revoke access, just delete the session record. This makes logout, user bans, and forced password resets incredibly easy.
They’re also secure by design. Since the cookie contains only a randomly generated ID (not actual user data), there’s less risk of sensitive information leaking or being tampered with.
And in traditional web apps (especially those rendered on the server), sessions feel almost effortless. You log in, get a cookie, and everything “just works seamlessly.”
Where Sessions Start to Struggle — (Statefullness)
The simplicity that makes sessions elegant is also what makes them tricky to scale. Because session data lives on the server, you need to share state across multiple servers in a distributed system.
That’s why many modern setups add Redis or another shared store in the middle — effectively turning your “simple” approach into a mini distributed cache system.
Sessions also don’t play nicely with every architecture. If you’re building APIs for mobile apps, or you’re serving multiple clients (like web + mobile + third-party integrations), cookies become less convenient. You have to start managing CORS, cookie domains, and CSRF protection manually — and that’s where many developers start reaching for JWTs.
Still, for many use cases, sessions remain the most reliable and controlled method of managing authentication. They might be “old-school,” but they’re far from obsolete.
3. JWT-Based Authentication — The Modern Minimalist
If sessions are the “guest list at the door,” JWTs (JSON Web Tokens) are more like stamped tickets — portable, self-contained, and designed for a world where one login might serve many doors.
Why JWTs Became So Popular
As applications evolved from monoliths into distributed systems, and from simple web browsers to APIs, microservices, and mobile apps, traditional sessions began to feel heavy. Developers needed a way to verify users without constantly querying the database.
JWTs promised exactly that:
- Statelessness: the server doesn’t need to store session data. Everything it needs to know about the user is embedded in the token itself.
- Cross-service compatibility: APIs, mobile apps, and third-party integrations can all verify the same token using a shared secret or public key.
- JSON payload flexibility: tokens can contain small bits of user data (like
user_id,role, orpermissions), which makes them easy to decode and use across services.
This made JWTs incredibly attractive in the age of microservices and single-page apps (SPAs). Especially when performance and horizontal scaling mattered.
The Strengths
JWTs shine in environments where speed and autonomy are key.
A token can be verified instantly, without hitting a database or cache. That makes it perfect for API-driven systems or multi-service architectures where each service needs to trust the same authentication source.
They’re also portable — once issued, a JWT can move between services, clients, and even different domains with minimal friction. That’s why they became a favorite for companies building flexible, distributed systems.
The Trade-offs However…
But that same portability is also their greatest weakness.
A JWT, once issued, is valid until it expires — even if the user logs out, changes their password, or gets compromised. You can’t just “delete” a JWT like you would a session. It’s a stamped ticket: once it’s out there, you can’t easily pull it back.
To fix this, developers often add refresh tokens, blacklists, or short-lived access tokens, which ironically brings some of the “statefulness” back that JWTs were supposed to avoid.
JWTs also introduce more surface area for mistakes — from misconfigured expiration times, to unencrypted payloads, to unsafe local storage on the client.
So while JWTs feel cleaner and faster, they require more careful design to stay secure — especially when you start handling token rotation and revocation.
4. The Misconceptions
If you’ve been around long enough, you’ve probably seen these statements thrown around like gospel truths. I did too — until I tried to build something real.
Here are the three biggest misconceptions I kept seeing (and believing) before the Auth Kit forced me to unlearn them.
“JWTs don’t need a database.”
This one sounds elegant — the idea that you can go fully stateless and never hit your database again. Just sign the token, verify it later, and you’re done.
But the moment you need to add features like logout, refresh token rotation, or multi-device session tracking, that fantasy collapses.
To invalidate a JWT before it expires, you need a revocation list or a refresh store — and that means a database or cache. If a user resets their password or logs out from one device, you can’t just let old tokens float around until expiry.
In practice, most systems that claim to be “stateless” still end up introducing state — they just call it something else.
“Sessions don’t scale.”
This was true once, back when every session lived in the memory of a single web server. If that server went down, everyone was logged out.
But that was before Redis and distributed caching became standard. Modern systems can scale session-based authentication horizontally with little effort.
It’s not the “session” model that doesn’t scale — it’s the old way of storing sessions that didn’t. Today, you can run millions of sessions in memory across clusters with virtually no overhead.
“JWTs are more modern.”
This is my favorite one, because it’s half true and half marketing. JWTs are modern in the sense that they solve new problems: distributed systems, APIs, and cross-domain authentication.
But “modern” doesn’t mean “better.” It just means different trade-offs. JWTs shift complexity away from the server and toward design — token lifetimes, refresh cycles, and revocation logic all become your responsibility.
When you start implementing those features manually, you realize JWTs aren’t the “simpler” path. Its just a different flavor of complexity.
I believed all three of these. And it wasn’t until I tried to implement logout, rotation, and device tracking in the Auth Kit that I understood how intertwined JWTs and sessions really are.
That realization led me to explore a hybrid approach — one that borrows the strengths of both, and avoids the weaknesses of either.
5. Why a Hybrid Approach Makes Sense
Eventually, I stopped seeing JWTs and Sessions as opposites, and started seeing them as layers.
JWTs are fast, portable, and great for distributed systems. Sessions are secure, controllable, and easy to reason about.
Instead of choosing one, I realized the better question was: what if we used both, in their strongest roles?
That’s the idea behind the hybrid approach, where JWTs handle stateless authentication at the edge, and sessions (or database-backed records) maintain the state you actually care about: refresh tokens, devices, and revocation.
5.1 Refresh Token Rotation
A common mistake in many systems is to issue long-lived JWTs as refresh tokens with no server-side tracking. It feels convenient at first — until one gets leaked.
If the backend isn’t supervising refresh tokens, an attacker who steals one can keep generating new access tokens indefinitely, and you’d have no way to stop them.
In a hybrid model like the one I used in the Auth Kit, the refresh token is backed by a server record.
When a user logs in, the server issues a refresh token (valid for seven days) and stores it in the database. Each time the client requests a new access token, the server verifies that the refresh token matches the one currently stored. If it doesn’t, the request is rejected.
This ensures that only the most recent refresh token is valid for each session — giving you replay protection without keeping an entire history of tokens.
You still get the scalability benefits of short-lived, stateless JWT access tokens, while the server quietly supervises the refresh layer in the background.
It’s a balance between convenience and control: fast validation at the edge, but with a trusted source of truth at the core.
5.2 Multi-Device Session Tracking
In real-world applications, users don’t just log in once. They log in from multiple devices — their laptop, phone, or tablet. Each device should have its own independent session that can be managed separately.
This is where a pure JWT setup starts to fall apart. Because JWTs are stateless, you don’t really know which tokens exist, or where they came from. You can’t list active sessions, or selectively revoke just one device’s access.
A hybrid approach fixes this by storing session metadata in a database — each refresh token is tied to a specific device or client. That allows you to:
- Show users their active sessions (“You’re logged in on Chrome, iPhone, iPad”)
- Allow one-click “Logout from all devices”
- Detect suspicious logins or simultaneous access patterns
From a UX perspective, it gives users visibility. From a security standpoint, it gives you control.
JWTs alone can’t give you that kind of accountability — because they were never meant to store state awareness.
5.3 Database-Backed Revocation
Revocation is the Achilles’ heel of pure JWT systems. Once a token is out, you can’t easily take it back before it expires.
By tying every token (access or refresh) to a database or in-memory session record, you gain a built-in kill switch.
If you detect compromise, you just delete or flag that record, and all tokens associated with it immediately become invalid.
This is how large platforms like Slack, GitHub, and Stripe handle authentication. They issue JWTs for fast API authentication but still maintain server-side records for refresh tokens and revocation tracking.
It’s not “extra complexity.” It’s the foundation that makes secure, multi-device systems possible.
In the end, I realized something simple:
JWTs give speed. Sessions give control. The hybrid gives both.
It’s not about choosing between “modern” and “traditional.” It’s about designing authentication that fits the realities of distributed, user-facing systems — where performance and safety have to coexist.
6. Conclusion — The Real Lesson
When I started building the Auth Kit, I thought authentication was a simple binary: either go stateless with JWTs or stateful with sessions. I wanted the elegance of JWTs and the simplicity of sessions — but it took actually implementing both to realize they were never meant to compete.
They solve different problems. JWTs scale outward — across services, APIs, and clients. Sessions scale inward — managing trust, safety, and control from the server side.
Once I saw them that way, the architecture started to make sense.
The hybrid model wasn’t a hack or a workaround — it was a natural evolution of how modern systems handle identity.
If you look closely, you’ll see the same pattern everywhere:
- Slack uses JWTs for API calls but tracks refresh tokens in its database.
- GitHub and Stripe rotate tokens and log device sessions server-side.
- Even Google’s OAuth implementation blends both — JWTs for access, sessions for persistence.
The best systems don’t choose between stateful or stateless — they balance both, intentionally.
In the next part, I’ll go hands-on and show how I implemented this hybrid model inside the NestJS Auth Kit — combining stateless JWT access with stateful session refresh handling, rotation, and revocation.
I’ve packaged everything I built — the NestJS API, the Next.js frontend, and a detailed README — into a free Auth Kit. You can request access here: onakoyakorede.cc/template-kit/full-stack-auth-kit
Further Reading
If you want to go deeper into the philosophy and practical trade-offs of JWTs vs Sessions, here are some excellent reads that shaped my thinking while building the Auth Kit:
- Stop using JWT for sessions — Sid Krishnan
- Sessions vs Tokens: The Definitive Guide — Hasura Engineering
- The JWT + Session Hybrid Model— Clerk Blog