System Design
What Are JSON Web Tokens (JWTs)?
JWTs are how modern applications carry proof of who you are without the server having to remember you. Three dot-separated parts, one elegant idea - and a few sharp edges. Here's how they actually work and when to use them.
When you log into a modern web app and then make request after request without re-entering your password each time, something is carrying proof of who you are with each request. Increasingly, that something is a JSON Web Token (JWT) - a compact, self-contained token that holds your identity and can be verified by the server without the server having to remember you at all. That last part is the elegant trick: JWTs enable stateless authentication. This guide explains what a JWT is, its structure, how it works, and the sharp edges to know about.
What a JWT is
A JWT is a compact, self-contained token for securely transmitting information (claims) between parties, most commonly used for authentication and authorization. "Self-contained" is the key word: the token itself carries the information (who the user is, what they can do, when it expires), and it's signed so the receiver can verify it hasn't been tampered with. The server doesn't need to look anything up to trust a valid JWT - the token carries its own proof.
This is what makes JWTs popular for stateless authentication: instead of the server keeping session data and looking you up on every request (stateful), it issues you a signed token that contains your identity, and on each request it just verifies the token's signature. No server-side session storage, no database lookup to check who you are - the token proves it. That statelessness is a big deal for scaling, which we'll come back to, and it's the single property that explains why JWTs rose alongside microservices and distributed APIs.
The structure: three parts
A JWT is a string with three parts separated by dots: header.payload.signature. Each part is Base64URL-encoded (a way of representing data as a URL-safe string - note this is encoding, not encryption; we'll return to that). The three parts:
1. Header. Metadata about the token - primarily the algorithm used to sign it (so the receiver knows how to verify the signature) and the token type. Small and simple.
2. Payload. The claims - the actual information the token carries. This includes things like the user's ID, perhaps their roles or permissions, and standard fields like an expiration time (exp) saying when the token is no longer valid, who issued it, and when. The payload is the meat of the token: the data being transmitted.
3. Signature. The cryptographic signature that makes the token trustworthy. It's created by signing the header and payload together with a key, so that any change to the header or payload would invalidate the signature. This is what lets the receiver verify the token is authentic and untampered.
graph LR
JWT["JWT string"] --> H["Header<br/>(algorithm, type)"]
JWT --> P["Payload<br/>(claims: user id, exp, roles)"]
JWT --> S["Signature<br/>(signs header + payload)"]
H -.->|verifies| S
P -.->|verifies| S
This diagram shows a JWT's three parts. The JWT string is divided into a header, a payload, and a signature, separated by dots. The header contains metadata like the signing algorithm and token type. The payload contains the claims - the user's ID, an expiration time, roles, and other information. The signature is created by signing the header and payload together, so any tampering with either invalidates it. The signature is what lets the receiver verify the header and payload are authentic and unaltered.
How the signature creates trust
The signature is the clever part. When the server issues a JWT, it creates the signature by combining the header and payload and signing them with a key (a secret known only to the server, or a private key). When the server later receives the token back, it recomputes the signature from the header and payload using the same key and checks that it matches the signature in the token.
If they match, the server knows two things: the token was issued by it (only it has the key to produce a valid signature), and the header and payload haven't been altered (any change would produce a different signature, failing the check). If someone tampered with the payload - say, changed the user ID to impersonate someone, or extended the expiration - the signature wouldn't match, and the server would reject the token.
This is why the server doesn't need to store anything: the signature, verifiable with the server's key, is the proof that the token is genuine and unmodified. The token carries its own trustworthiness.
The authentication flow
Here's how JWTs work in a typical login flow:
- Login. You log in with your credentials. The server verifies them and creates a JWT containing your identity (and an expiration), signs it, and sends it back to you.
- Store. Your client (browser/app) stores the token.
- Request. On each subsequent request, your client sends the JWT (typically in an HTTP header).
- Verify. The server verifies the token's signature (using its key) and, if valid and not expired, trusts the identity in the payload - granting access. No database lookup, no session store; just signature verification.
So after login, every request carries its own proof of identity in the token, and the server validates it statelessly. This is the core appeal: authentication without server-side session state.
Why stateless matters: the scaling advantage
The stateless property is JWT's headline benefit, and it matters for scaling. With traditional server-side sessions, the server stores session data and looks you up on each request - which means either all requests must go to the server holding your session (sticky sessions, limiting load balancing) or all servers must share a session store (added complexity). With JWTs, any server can verify any token using the shared key, with no session lookup. So you can scale out to many servers behind a load balancer, and any of them can handle any request, because the token is self-contained.
This fits naturally with stateless, horizontally-scalable architectures, microservices, and APIs - which is a big reason JWTs became popular. The token travels with the request and proves identity anywhere, so the auth layer doesn't constrain how you distribute load. (See the stateful vs stateless guide.)
JWT vs. traditional session cookies
To really understand JWTs, compare them to the traditional alternative: server-side sessions with cookies.
In the traditional model, when you log in, the server creates a session (storing your data server-side) and gives you a session ID in a cookie. On each request, you send the cookie, and the server looks up your session by that ID. The identity lives on the server; the cookie is just a key to find it.
With JWTs, the identity lives in the token itself (carried by you), verified by signature rather than lookup. The contrast:
| Server-side sessions | JWTs | |
|---|---|---|
| Where state lives | On the server (session store) | In the token (client holds it) |
| Verification | Look up session by ID | Verify signature, no lookup |
| Scaling | Needs shared session store or sticky sessions | Any server verifies any token |
| Revocation | Easy (delete the session) | Hard (token valid until expiry) |
| Size | Tiny cookie (just an ID) | Larger token (carries claims) |
Neither is universally better - it's a trade-off. Sessions make revocation trivial but require shared state for scaling; JWTs scale statelessly but make revocation hard. The right choice depends on whether easy revocation or stateless scaling matters more, and many systems combine them. Understanding this contrast is understanding why you'd reach for a JWT in the first place - and when a plain session might serve better.
Where to store a JWT (and the security stakes)
Once the client has a JWT, where does it keep it? The common options each have trade-offs:
- In a cookie (ideally HTTP-only, so JavaScript can't read it) - protects the token from script-based theft (cross-site scripting), but requires care against a different attack class (cross-site request forgery).
- In browser storage (local storage) - convenient and accessible to JavaScript, which means a script-injection vulnerability could steal the token.
The key point: a JWT is a bearer token - whoever holds it can use it. So protecting it in transit (always over HTTPS) and at rest (storing it safely) is essential, because a stolen token grants access until it expires. This is why the storage choice matters and why short expirations limit the damage of theft. The token's power - carrying its own proof of identity - is also its risk: it must be guarded like the credential it is.
Signed vs. encrypted: JWS and JWE
A clarification that resolves a common confusion: the standard JWT described here is signed (technically a JWS - JSON Web Signature). Signing makes it tamper-evident (you can verify it wasn't altered) but not private (the payload is readable). There's also an encrypted variant (JWE - JSON Web Encryption) where the payload is encrypted and unreadable without the key.
In practice, most JWTs you encounter are signed-but-readable (JWS), which is why the "don't put secrets in the payload" rule matters. If you genuinely need the contents hidden, encryption (JWE) is the tool - but it's less common; the usual pattern is to keep non-sensitive claims in a signed JWT and not put anything secret in it. Knowing that "JWT" usually means "signed and readable, not encrypted" is exactly the distinction that prevents the dangerous mistake of treating a standard JWT's payload as private.
Common JWT mistakes
Because JWTs are easy to use but easy to misuse, a few mistakes recur:
- Putting sensitive data in the payload. The most common and most dangerous: the payload is readable, so passwords, secrets, or private data must never go in it.
- No expiration (or too-long expiration). A JWT without a short expiration is a token that, if stolen, works forever. Always set a reasonable, short expiration, given that revocation is hard.
- Weak or exposed signing keys. The whole security rests on the signing key. A weak (guessable) or leaked key lets attackers forge valid tokens. Use a strong key and guard it.
- Trusting the algorithm field blindly. The token's header declares its signing algorithm, and historically there were attacks where an attacker changed it (for instance, to "none," claiming no signature is needed). A correct implementation enforces the expected algorithm rather than trusting whatever the token claims - which is why using a well-maintained, audited library rather than hand-rolling verification is strongly recommended.
- Using JWTs where sessions would be simpler. For a single traditional web app that doesn't need stateless scaling, plain server-side sessions are simpler and make revocation trivial. Reaching for JWTs reflexively, when their main benefit isn't needed, takes on their downsides for no gain.
The theme: JWTs are powerful but unforgiving of misuse - and the safe path is short expirations, no secrets in the payload, strong guarded keys, a well-tested library, and using them where their statelessness actually earns its keep.
The sharp edges
JWTs are powerful but have real gotchas that you must know to use them safely:
- The payload is readable - not encrypted. A standard JWT is signed, not encrypted. The payload is only Base64-encoded, which anyone can decode and read. So never put sensitive data in a JWT payload - no passwords, no secrets - because it's effectively public to anyone who has the token. The signature protects against tampering, not reading.
- Revocation is hard. Because JWTs are stateless and self-contained, the server doesn't track them - so you can't easily revoke a token before it expires. If a token is stolen, it remains valid until its expiration. This is the flip side of statelessness, and it's why JWTs usually have short expiration times (limiting the damage window) and why systems often pair them with refresh tokens or maintain a revocation list for emergencies (which reintroduces some state).
- Protect the signing key. The entire security rests on the signing key being secret. If it leaks, anyone can forge valid tokens. Guarding the key is paramount.
- Don't bloat the payload. Since the token travels with every request, a large payload adds overhead to every request. Keep claims minimal.
These aren't reasons to avoid JWTs - they're the conditions for using them correctly: short expirations, no sensitive data in the payload, a well-protected key, and a plan for revocation if needed.
Interview: JWTs come up in authentication and system design discussions. The strong answer covers the structure (header, payload, signature), how the signature enables stateless verification (the server checks the signature with its key, no lookup needed), and the scaling benefit (any server can verify any token, fitting horizontal scaling). The senior signal is naming the trade-offs: the payload is readable so no secrets go in it, and revocation is hard so tokens should be short-lived. Showing you know both the appeal (stateless, scalable) and the sharp edges (readable, hard to revoke) demonstrates real understanding.
The bottom line
A JWT is a compact, self-contained, signed token that carries identity claims, enabling stateless authentication. It has three dot-separated parts: a header (metadata and the signing algorithm), a payload (the claims - user ID, expiration, roles), and a signature (which signs the header and payload so tampering is detectable). On login the server issues a signed token; on each request the client sends it and the server verifies the signature with its key - no session lookup, so any server can validate any token, which is the scaling advantage. The sharp edges: the payload is readable (not encrypted), so no secrets in it; revocation is hard, so keep tokens short-lived; and the signing key must be guarded.
Used with those conditions in mind, JWTs are an elegant way to carry verifiable identity without server-side state. The mental model to keep: a JWT is a signed claim you carry with you - the server stamps it so it can't be forged or altered, hands it to you, and trusts it back because of the stamp, not because it remembers issuing it. That single shift, from "the server remembers who you are" to "you carry tamper-proof evidence of who you are," is what makes stateless authentication work and what makes JWTs scale - and also what creates every one of their sharp edges. Hold that model, respect the conditions, and you'll use JWTs well. We cover authentication and stateless design across System Design Fundamentals.
Go deeper
- JSON Web Token (Wikipedia) - the structure and standard.
- JWT.io - Introduction - a clear, canonical explanation with examples.
- RFC 7519 - JSON Web Token - the official specification.
- The System Design Primer - authentication in a system design context.