JWT vs. Session Tokens: What's Actually in a JWT, and What Isn't

Published July 8, 2026

JWTs are everywhere in modern web development, but there’s a surprising amount of confusion about what they actually are, what they contain, and when you should (or shouldn’t) use them. This guide digs into the structure, compares JWTs with traditional session tokens, and addresses the misconceptions that lead to security mistakes.

What’s Inside a JWT

A JWT (JSON Web Token) is a string made of three parts, separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

That’s it: three Base64url-encoded strings glued together with periods, no binary format, no special encoding scheme. Let’s break them apart.

Part 1: The Header

The first segment decodes to a JSON object describing the token itself:

{
  "alg": "HS256",
  "typ": "JWT"
}

The alg field tells the server which algorithm was used to create the signature. Common values include HS256 (HMAC with SHA-256, a symmetric algorithm using one shared secret) and RS256 (RSA with SHA-256, an asymmetric algorithm where a private key signs and a public key verifies). The typ field is almost always “JWT” and is technically optional.

Part 2: The Payload

The second segment is where your actual data lives:

{
  "sub": "1234567890",
  "name": "John",
  "iat": 1516239022
}

These key-value pairs are called “claims.” Some are standardized (called registered claims): sub (subject, usually a user ID), iat (issued at, a Unix timestamp), exp (expiration time), iss (issuer), aud (audience). You can also add any custom claims you want: roles, permissions, email, whatever your application needs.

Part 3: The Signature

The third segment is a cryptographic signature computed over the header and payload. For HS256, it’s essentially:

HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)

The signature lets the server verify that the token hasn’t been tampered with. If someone changes even one character in the header or payload, the signature won’t match, and the server rejects the token.

Base64 Is Not Encryption

This is the single most important thing to understand about JWTs: the header and payload are not encrypted. They’re encoded. Base64 is a way to represent binary data as text characters. It’s reversible by anyone with no key or password needed.

Paste any JWT into a decoder, and you’ll see the full contents of the header and payload in plain text. That means every claim you put in a JWT (user IDs, email addresses, roles) is visible to anyone who has the token. That includes the end user, anyone sniffing network traffic (if you’re not using HTTPS), and any JavaScript running on the page if you store the token in a place scripts can access.

There is a separate standard called JWE (JSON Web Encryption) that does encrypt the payload. But when people say “JWT,” they almost always mean JWS (JSON Web Signature): signed, but not encrypted.

How Server-Side Sessions Work

To understand why JWTs exist, you need to understand what came before them. Traditional session-based authentication works like this:

  1. User logs in with their credentials.
  2. Server creates a session object in a data store (database, Redis, memory) with a random ID.
  3. Server sends the session ID to the browser as a cookie.
  4. On every subsequent request, the browser sends the session ID cookie.
  5. Server looks up the session ID in the data store to find out who the user is and what they’re allowed to do.

The session ID itself is opaque, just a random string like a3f8b2c1d4e5. It contains no information. All the data lives on the server.

The Real Tradeoffs

Stateless vs. Stateful

The primary selling point of JWTs is statelessness. The server doesn’t need to store anything. It receives a token, verifies the signature, reads the claims, and knows who the user is, all without touching a database or cache.

Server-side sessions are stateful. Every request requires a lookup in whatever store holds the session data. If that store goes down, every user is effectively logged out.

In practice, the statelessness benefit is most significant in distributed systems. If you have 20 servers behind a load balancer, sessions require either sticky sessions (routing each user to the same server) or a shared session store (like Redis). JWTs require neither: any server with the signing key can verify any token.

Token Size

A session cookie is typically 30-50 bytes. A JWT, even a minimal one, is usually 200-800 bytes, and can easily grow larger as you add claims. This matters because the token is sent with every HTTP request. If you’re making dozens of API calls per page load, that overhead adds up. It’s not usually a dealbreaker, but it’s not zero.

Revocation: The Hard Problem

This is where JWTs have a genuine weakness. If you need to immediately invalidate a user’s access (they changed their password, you detected suspicious activity, an admin banned them), a server-side session is simple: delete the session from the store. The next request fails because the session ID doesn’t exist anymore.

With JWTs, you can’t “delete” a token. It’s a self-contained string living on the client. It’s valid until it expires, and the server has already decided not to maintain a data store. Your options are:

  • Short expiration times: Make tokens expire in 5-15 minutes and use refresh tokens to issue new ones. This limits the damage window but doesn’t eliminate it.
  • Token blocklists: Maintain a list of revoked token IDs (jti claim). But now you’re checking a data store on every request, and you’ve given up the statelessness that was the whole point.
  • Versioning: Store a token version per user in the database; reject tokens with old versions. Again, this requires a database lookup.

There’s no clean solution here. If instant revocation matters to your application, either use short-lived JWTs with refresh tokens (the most common approach) or use server-side sessions.

Where You Store Them

Session IDs live in cookies, typically with HttpOnly, Secure, and SameSite flags. This means JavaScript can’t access them, they’re only sent over HTTPS, and they have some protection against cross-site request forgery (CSRF).

JWTs get stored in a few places, each with tradeoffs:

  • HttpOnly cookies: Same protections as session cookies. You need CSRF protection but are safe from XSS token theft. This is generally the recommended approach for web applications.
  • localStorage: Persists across tabs and browser restarts. But any JavaScript on the page, including third-party scripts and XSS payloads, can read it. If an attacker injects a script, they can steal the token and use it from any device.
  • sessionStorage: Same XSS risk as localStorage but doesn’t persist across tabs.
  • In memory (JavaScript variable): Safe from XSS in other contexts, but the token disappears on page refresh.

Common Misconceptions

“JWTs Are More Secure Than Sessions”

Neither is inherently more secure. They have different attack surfaces. JWTs stored in localStorage are vulnerable to XSS. Session cookies are vulnerable to CSRF (though SameSite cookies have mostly addressed this). The security depends on your implementation, not the technology choice.

“You Should Put Everything in the JWT”

Just because you can add custom claims doesn’t mean you should. Every claim increases the token size, is visible to the client, and is frozen at the moment the token was issued. If a user’s role changes, their JWT still says the old role until it expires. Keep JWTs lean. A user ID and expiration are often sufficient; look up everything else from the database when needed.

“JWTs Replace Cookies”

JWTs and cookies are different things. A cookie is a transport mechanism, a way for the browser to send data with every request. A JWT is a token format. You can (and often should) send JWTs inside cookies. They’re not competing concepts.

“Base64 Provides Some Security”

It doesn’t. Base64 encoding takes about two seconds to reverse. Every programming language has a built-in function for it, and there are dozens of online decoders. Never put sensitive data (passwords, API keys, credit card numbers) in a JWT payload.

When to Use Which

Use server-side sessions when: you need instant revocation, your application is a traditional server-rendered web app, you don’t have complex cross-service authentication, or you want simplicity.

Use JWTs when: you’re building a distributed microservices architecture where multiple services need to verify identity independently, you’re implementing OAuth 2.0 or OpenID Connect flows, or you need short-lived tokens for specific operations (like password reset links or email verification).

Don’t use JWTs because: “everyone uses them” or “they’re more modern.” That’s not a technical reason, and sessions are still the right choice for many applications.

Try the JWT Decoder to inspect the contents of any JWT, or use the Base64 Encoder/Decoder to see for yourself how easily Base64 content can be decoded.

Related Tools