When I first learned JWT, I thought it was just "a library to create tokens for authentication." I used it in every project. I knew how to use it. But I never understood why it exists, why it is designed the way it is, and why everyone uses it instead of other approaches.
That changed when I stopped watching tutorials and started asking "why" about real numbers.
First, I Looked at the Numbers
I measured the minimum time common authentication operations take on my system:
- JWT signing: ~5ms - pure CPU, no waiting
- MongoDB query: ~50ms - network + disk
- Bcrypt hash creation: ~100ms - intentional CPU work
- Bcrypt comparison: ~100ms - same as creation
These numbers look simple. But they contain the entire story of why JWT exists.

This image shows a query for a hashed password.

This image shows a query for a JWT signature

This is image for data query from database
There is a data inconsistency because I missed recording the minimum system time during the experiment.
The Old Way — Sessions
Before JWT, most apps used sessions.
Here is how session authentication works:
- User logs in with correct password
- Server creates a session — stores { sessionId: "abc123", userId: "vikrant", loggedIn: true } in its database or memory
- Server sends the sessionId to the browser as a cookie
- Every subsequent request, the browser sends that cookie automatically
- Server looks up the sessionId in the database, finds the user, confirms they are logged in
This works. But look at step 5. Every single request requires a database lookup. That is 50ms of I/O on every request, just to verify who you are. Not to get your data. Not to do the actual work. Just to confirm your identity.
Now imagine 10,000 users making requests simultaneously. That is 10,000 database calls per second just for authentication. Your database becomes a bottleneck. Your server slows down. And if that session database goes down - nobody can log in. Your entire authentication system has a single point of failure.
Why Not Just Use Bcrypt Directly?
When I realized sessions expose user data to database lookups, I thought - why not just hash the userId with bcrypt and send that directly to the user? The user sends it back with every request, we compare it, done. No sessions, no database lookup for auth
Then I looked at the numbers again.
Bcrypt takes 100ms per operation. On purpose. It is intentionally slow to make brute force attacks expensive. If you used bcrypt for identity verification on every request, with 1000 concurrent users you would need 100 seconds of compute per second. Your server collapses.
Bcrypt is the right tool for storing passwords. It is catastrophically wrong for per-request authentication.
What JWT Actually Solves
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three parts separated by dots:
Header - algorithm used to create the signature
{ "alg": "HS256", "typ": "JWT" }
Payload - your data
{ "userId": "123", "role": "user", "exp": 1700000 }
Signature - mathematical proof of integrity
HMAC_SHA256(base64(header) + "." + base64(payload), SECRET_KEY)
When a request arrives, the server does not look up anything in a database. It just recomputes the signature using the same secret key and checks if it matches. Pure mathematics. 5ms. No I/O. No database. No network call.
The Security Model - What JWT Actually Guarantees
The payload is not secret. It is only base64 encoded — which anyone can decode. Open jwt.io and paste any JWT. You will see the payload immediately without any key.
This means: never put passwords, sensitive data, or anything private in a JWT payload.
But the payload is tamper-proof. If someone changes "role": "user" to "role": "admin" in the payload, the signature will no longer match — because the signature was computed on the original payload. They cannot fix the signature either because they do not have your secret key.
JWT gives you:
- No confidentiality - payload is readable by anyone
- Full integrity - payload cannot be modified without detection
- Authentication - valid signature proves the token came from your server
The One Real Weakness of JWT
JWT cannot be revoked easily.
With sessions, logging someone out is trivial — delete the session from the database. Done.
With JWT, the server has no record of issued tokens. If a token is stolen or you want to force logout, you cannot invalidate it until it expires naturally.
The solutions engineers use:
Short expiry + refresh tokens - Access tokens expire in 15 minutes. A separate refresh token (stored in database) issues new access tokens. Revoke the refresh token to cut off access within 15 minutes maximum.
Token blacklist in Redis - Store revoked token IDs in Redis. Check on every request. Fast enough to not lose much performance, but adds back a small I/O step.
What I Actually Learned
I used JWT in three projects before understanding any of this. I knew the syntax. I could follow the tutorial. But I could not have explained why sessions do not scale, why bcrypt cannot be used for per-request auth, why the payload is visible, or what happens to the thread pool under load.
The difference between knowing how to use something and understanding why it exists is the difference between copying code and engineering.
The numbers made it real. 5ms versus 50ms versus 100ms are not just performance statistics. They are the reason JWT was invented.
Top comments (0)