How to Invalidate a JWT Token

Here's the uncomfortable truth: once you issue a JWT, you can't "delete" it. But you can make it useless. Here's how.

When I first built a logout feature with JWT, I thought deleting the token from localStorage was enough.

It wasn't. The token still worked perfectly fine on the server. If someone had copied it before logout, they could keep using it until it expired.

That's the awkward thing about JWTs. They're stateless. Once issued, your server has no way to "call them back" the way it can destroy a session in a database.

But there are real solutions. Let me walk you through them.

Why You Can't Just "Delete" a JWT

A JWT is a self-contained token. All the information needed to verify it — the signature, the expiry, the user data — is baked right into the token itself.

Your server doesn't keep a list of "active tokens." It just checks whether the token's signature is valid and whether it has expired. That's it.

So if the token is valid and not expired, your server will accept it. Doesn't matter if the user "logged out." The token itself doesn't know that.

This is actually a feature, not a bug. Stateless auth scales really well. But it does create a real problem for things like logout and account suspension.

Method 1: Short Expiry Times (The Simplest Fix)

The most common approach is to just keep your access tokens short-lived.

If your token expires in 15 minutes, a stolen token is only useful for 15 minutes. That's not perfect, but it limits the damage significantly.

const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '15m' } // Token is useless after 15 minutes );

Combine this with a refresh token system and you get the best of both worlds. Users stay logged in, but the actual access token is always fresh and short-lived.

For many apps, this is genuinely enough. No extra infrastructure needed.

Method 2: Token Blacklisting

If you need to invalidate a specific token immediately — like when a user reports a stolen device — you need a blacklist.

The idea is simple. When a token is "logged out" or revoked, you save it somewhere. Every request checks that list. If the token is on it, reject it.

// Simple in-memory blacklist (use Redis in production) const tokenBlacklist = new Set(); // Add to blacklist on logout app.post('/api/logout', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (token) { tokenBlacklist.add(token); } res.json({ message: 'Logged out successfully' }); }); // Check blacklist in your auth middleware function verifyToken(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'No token provided' }); } // Reject blacklisted tokens immediately if (tokenBlacklist.has(token)) { return res.status(401).json({ error: 'Token has been revoked' }); } jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { if (err) { return res.status(403).json({ error: 'Invalid token' }); } req.user = decoded; next(); }); }

This works, but there's a catch. A blacklist that lives in memory gets wiped every time your server restarts. For production, you need something persistent.

Use Redis for a Production Blacklist

Redis is the most common choice here. It's fast, it persists data, and it even lets you auto-expire entries so your blacklist doesn't grow forever:

const redis = require('redis'); const client = redis.createClient(); // On logout - store token until it naturally expires app.post('/api/logout', async (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (token) { const decoded = jwt.decode(token); const timeLeft = decoded.exp - Math.floor(Date.now() / 1000); // Only blacklist if token hasn't already expired if (timeLeft > 0) { await client.setEx(`blacklist:${token}`, timeLeft, 'revoked'); } } res.json({ message: 'Logged out' }); }); // Check Redis in middleware async function verifyToken(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'No token provided' }); } const isBlacklisted = await client.get(`blacklist:${token}`); if (isBlacklisted) { return res.status(401).json({ error: 'Token revoked' }); } jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { if (err) return res.status(403).json({ error: 'Invalid token' }); req.user = decoded; next(); }); }

The clever part is setEx — it automatically removes the blacklist entry after the token's natural expiry time. So your Redis store never fills up with old, already-expired tokens.

Method 3: Token Versioning

This is my favourite approach for apps that need to invalidate all tokens for a specific user. Like when someone changes their password or you need to force a user out of all devices.

The idea: store a version number per user in your database. Include that version in every token. On each request, check if the token's version matches the current version in the database.

// When creating token, include the user's current token version const token = jwt.sign( { userId: user.id, tokenVersion: user.tokenVersion // stored in your database }, process.env.JWT_SECRET, { expiresIn: '1h' } ); // In your auth middleware async function verifyToken(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; jwt.verify(token, process.env.JWT_SECRET, async (err, decoded) => { if (err) return res.status(403).json({ error: 'Invalid token' }); // Check version against database const user = await User.findById(decoded.userId); if (user.tokenVersion !== decoded.tokenVersion) { return res.status(401).json({ error: 'Token invalidated' }); } req.user = decoded; next(); }); } // To invalidate ALL tokens for a user, just bump their version async function invalidateAllUserTokens(userId) { await User.findByIdAndUpdate(userId, { $inc: { tokenVersion: 1 } // increment by 1 }); }

When you want to kick someone out of all their devices — password change, account compromise, admin ban — just increment their tokenVersion. Every existing token they have instantly becomes invalid.

Clean. No blacklist to manage. No Redis required.

Method 4: Rotate Your Secret Key (Nuclear Option)

If your secret key gets compromised, or you need to invalidate every single token across your entire app, change your JWT secret key.

Every token ever issued with the old key becomes invalid the moment you deploy the new key. Your server can no longer verify them. They're all dead.

I keep jwtsecretkeygenerator.com bookmarked specifically for this. When I need a fresh key fast, I go there, generate one, and update my environment variable. Takes two minutes.

The downside: every single user gets logged out. Use this when security is the priority and you can accept that disruption.

Which Method Should You Actually Use?

Depends on what you need. Here's a simple way to think about it:

Situation Best Method
Basic logout, low-risk app Short expiry + delete from client
Immediate single-token revocation Token blacklist with Redis
Logout from all devices Token versioning in database
Password change security Token versioning
Secret key compromised Rotate the secret key
Ban / suspend a user Token versioning or blacklist

For most regular apps, token versioning is the cleanest solution. It handles the most common scenarios without needing extra infrastructure like Redis.

What About Just Clearing the Cookie or localStorage?

Clearing the token on the client side is still worth doing. It means the honest user's browser no longer has the token. They can't use it by accident.

But it's not enough on its own.

If someone copied the token before logout — through an XSS attack, a network sniff, a shoulder surf at a coffee shop — clearing it from your own browser does nothing to stop them.

Client-side cleanup is step one. Server-side invalidation is step two. You need both.

💡 Practical Recommendation: For most apps, use short-lived access tokens (15 min) + refresh token versioning. You get automatic expiry, clean logout, and "log out all devices" without needing Redis. Add Redis blacklisting only if you need instant single-token revocation.

The Foundation Still Matters

All these invalidation strategies only work if your JWT secret key is strong in the first place. A weak key means someone can forge tokens no matter how good your invalidation logic is.

Before worrying about invalidation strategies, make sure your secret is solid. Generate it properly, store it in environment variables, and never reuse it between projects.

Get the basics right first. Then build on top of them.