JWT Finally Made Sense to Me (After Breaking Production)
What I learned about JWT tokens after making every mistake possible

So I broke production yesterday. Not badly, just… everyone got logged out at 3 PM and I had no idea why.
Turns out I had set JWT expiration to 1 hour when I thought I was setting it to 24 hours. Classic. But while debugging this mess, I actually learned what JWT does instead of just copy-pasting code from Stack Overflow like I’d been doing.
If you’re using JWT without really understanding it, this is what I wish someone had explained to me.
The Concert Wristband Thing
Everyone uses this analogy but it actually helped me finally get it.
You go to a music festival. At the entrance, they scan your ticket and give you a wristband. That wristband has some holographic pattern that only they can make. Now you can go to any stage, any bar, anywhere in the venue. The staff just check your wristband — they don’t need to verify your original ticket again.
JWT is that wristband. Your server gives it to you when you log in, and then you show it with every request instead of logging in again.
The holographic pattern? That’s the signature. Only the server can create it because only the server has the secret key.
Why Not Just Use Sessions?
I used sessions for my first project. Here’s what happened:
User logs in → Server stores session in database → Server sends session ID as cookie → Every request checks database for that session ID.
This worked fine on my laptop. Then we deployed to two servers for redundancy. User logs in on Server A. Their next request hits Server B. Server B checks database: “Who is this person?”
Yeah we could use sticky sessions or Redis or whatever. But JWT just… skips all that. The token itself contains everything needed. No database lookup. No shared state. Any server can verify it independently.
That’s when JWT clicked for me. It’s not about security (sessions can be just as secure). It’s about not needing to store session state.
What’s Actually in That Weird String
A JWT looks like garbage:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
But it’s actually three parts separated by dots.
Go to jwt.io right now and paste any JWT. You’ll see it decoded. That’s when I had my “oh crap” moment.
Part 1 (Header): Just says “this is a JWT signed with HS256”
Part 2 (Payload): Your actual data
json
{
"userId": 123,
"email": "whatever@example.com",
"iat": 1699123456,
"exp": 1699127056
}
Part 3 (Signature): The magic verification part
Here’s what freaked me out: the payload is NOT encrypted. Anyone can decode it and read it.
I had been storing user passwords in JWTs for like 3 months. Don’t do this.
The security is in the signature. You can READ the token, but you can’t CREATE a valid one without the secret key. And you can’t MODIFY it because that breaks the signature.
How I Actually Use It
Here’s my real code, not the cleaned-up tutorial version.
Server side (Node/Express):
javascript
const jwt = require('jsonwebtoken');
// Login route
app.post('/api/login', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user || !user.validPassword(req.body.password)) {
return res.status(401).json({ error: 'nope' });
}
const token = jwt.sign(
{ userId: user._id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' } // I learned my lesson
);
res.json({ token });
});// Middleware for protected routes
const auth = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'need token' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
next();
} catch (err) {
res.status(401).json({ error: 'bad token' });
}
};// Use it on protected routes
app.get('/api/profile', auth, async (req, res) => {
const user = await User.findById(req.userId);
res.json(user);
});
Client side (React):
javascript
// Store token after login
const login = async (email, password) => {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await res.json();
localStorage.setItem('token', data.token);
// yes I know localStorage has XSS issues, working on it
};
// Send token with requests
const getProfile = async () => {
const token = localStorage.getItem('token');
const res = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return res.json();
};
Is this perfect? No. But it works and I understand what’s happening now.
The Expiration Thing That Bit Me
JWTs can’t be “logged out.”
Once you create a token, it’s valid until it expires. Period. You can’t delete it like you delete a session from a database.
This is why my production incident happened. I set expiration to 1 hour thinking users could just get a new token. But I forgot to implement the refresh token flow. So at 4 PM, everyone who logged in at 3 PM got kicked out. Oops.
The standard solution is:
Short-lived access token (15–30 min)
Long-lived refresh token (7 days)
Store refresh token in database so you CAN revoke it
I haven’t implemented this yet. It’s on my list after “fix localStorage XSS vulnerability.”
Mistakes I Made (Learn From My Pain)
1. Putting sensitive data in the token
Passwords, API keys, credit card numbers — all readable by anyone. I was so dumb.
2. Using a weak secret
My first JWT secret was literally “secret”. Then “mysecret”. Then “mysecret123”.
Generate a proper one:
bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
3. Not checking expiration properly
I was checking if (decoded.exp) but not comparing it to current time. Expired tokens still worked. Great job me.
4. Sending tokens over HTTP
In development I was using HTTP. Tokens got intercepted. Always use HTTPS in production.
5. Making tokens too big
I was putting the entire user object in the token. Every request sent like 2KB of data. Just put the user ID and fetch the rest when needed.
When to Actually Use JWT
I’m not a JWT evangelist. It’s good for specific things:
Use JWT when:
You have multiple servers (or plan to)
You’re building an API that mobile apps will use
You want stateless architecture
You’re building a SPA
Don’t use JWT when:
You need instant logout everywhere
Your permissions change constantly
You’re building something simple
You need to track active sessions
For my side projects, I still use sessions because they’re simpler. For my work project with multiple servers, JWT makes way more sense.
Debugging Common Issues
When things go wrong (and they will):
“Invalid token” errors:
Secret key mismatch between sign and verify
Including “Bearer “ when you shouldn’t
Token expired
Token not being sent:
CORS settings
Missing
credentials: 'include'Cookie settings issues
Token doesn’t have expected data:
Console.log the decoded token
Check what you’re actually putting in jwt.sign()
What I’m Still Figuring Out
Real talk — I don’t have all the answers:
Where to actually store tokens (localStorage vs cookies vs memory)
How to implement refresh tokens properly
Best practices for XSS and CSRF protection
Key rotation strategies
Token blacklisting for emergency revocation
If you’ve solved these well, I’d genuinely appreciate hearing how. Still learning this stuff.
The Real Takeaway
JWT isn’t complicated. I made it complicated by not understanding why it exists.
It’s just a signed piece of data. The signature proves it came from you. The payload contains whatever you want (that’s not sensitive). The expiration prevents old tokens from working forever.
That’s it. Everything else is implementation details.
My production incident sucked but at least I finally understand what I’m doing instead of cargo-culting code from tutorials.
Start simple. Get basic login/logout working first. Don’t try to build the perfect auth system on day one.
And seriously, don’t store passwords in the payload. Learn from my mistakes, not by making them yourself.
Got questions or better approaches? Drop a comment — I’m definitely still learning this stuff and always looking to improve.

