Skip to content

JWT Blacklist — Revoking JWTs Before Expiration with Server-Side Blocklists

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about JWT Blacklist. We cover key concepts, practical examples, and best practices to help you master this topic.

JWT blacklisting is a technique for revoking JWTs before their natural expiry by maintaining a server-side list of revoked token IDs.

What You'll Learn

How JWT blacklisting works, when it is appropriate, implementing with Redis, and the trade-offs between blacklisting and short TTL.

Why It Matters

Stateless JWTs cannot be revoked — once issued, they are valid until expiry. For immediate revocation (user logs out, account suspended), blacklisting provides a mechanism. Without it, a stolen JWT is usable until it expires.

Real-World Use

Auth0 uses blacklisting for immediate token revocation. Keycloak maintains a token blacklist. Durga Antivirus Pro uses Redis-based blacklisting so security analysts can immediately revoke access when a partner integration is compromised.

flowchart LR
    A["Request + JWT"] --> B["Server"]
    B --> C{"In blacklist?"}
    C -->|"Yes"| D["401 Unauthorized"]
    C -->|"No"| E{"Signature valid?"}
    E -->|"Yes"| F["Process Request"]
    E -->|"No"| D
    style A fill:#dbeafe,stroke:#2563eb
    style B fill:#fef3c7,stroke:#d97706
    style D fill:#fecaca,stroke:#dc2626
    style F fill:#dcfce7,stroke:#16a34a

How Blacklisting Works

  1. Each JWT includes a unique jti (JWT ID) claim
  2. When a token needs revocation, its jti is added to a blocklist
  3. On each request, the server checks if the jti is in the blocklist
  4. Blocklist entries have TTL matching the token's remaining lifetime

Code Example: Redis-Based Blacklist

import redis
import jwt
import datetime
import uuid

r = redis.Redis(host="localhost", port=6379, decode_responses=True)
SECRET = "your-secret"

def issue_token(user_id):
    """Issue a JWT with unique jti."""
    jti = str(uuid.uuid4())
    payload = {
        "sub": user_id,
        "jti": jti,
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1),
        "iat": datetime.datetime.utcnow()
    }
    token = jwt.encode(payload, SECRET, algorithm="HS256")
    return token, jti

def revoke_token(jti, ttl_seconds=3600):
    """Add token to blacklist with TTL matching remaining lifetime."""
    r.setex(f"blacklist:{jti}", ttl_seconds, "revoked")

def is_revoked(jti):
    """Check if token is blacklisted."""
    return r.exists(f"blacklist:{jti}") > 0

# Middleware
@require_auth
def protected_endpoint():
    auth_header = request.headers.get("Authorization", "")
    token = auth_header[7:]

    try:
        payload = jwt.decode(token, SECRET, algorithms=["HS256"])
        jti = payload.get("jti")

        if not jti or is_revoked(jti):
            return jsonify({"error": "Token revoked"}), 401

        g.user = payload
    except jwt.ExpiredSignatureError:
        return jsonify({"error": "Token expired"}), 401
    except jwt.InvalidTokenError:
        return jsonify({"error": "Invalid token"}), 401

    return endpoint_function()

# Logout endpoint
@app.route("/api/auth/logout", methods=["POST"])
def logout():
    auth_header = request.headers.get("Authorization", "")
    token = auth_header[7:]

    try:
        payload = jwt.decode(token, SECRET, algorithms=["HS256"])
        jti = payload.get("jti")
        # Calculate remaining TTL
        exp = payload.get("exp")
        remaining = max(0, exp - datetime.datetime.utcnow().timestamp())
        revoke_token(jti, int(remaining))
    except Exception:
        pass

    return jsonify({"message": "Logged out"})

Blacklist vs Short TTL

Approach Pros Cons
Short TTL + Refresh No server state, scales infinitely Slight delay before revocation
Blacklist Immediate revocation Server state, Redis needed
Hybrid Balance of both More complex

Common Mistakes

1. Blacklisting Without TTL

If you never remove entries from the blacklist, it grows indefinitely. Always set TTL matching the token's remaining lifetime.

2. Not Including jti in Tokens

Without a unique identifier, you cannot blacklist individual tokens. Always issue each token with a unique jti.

3. Blacklisting Expired Tokens

Tokens past their exp are already invalid. Check expiry before checking the blacklist to save Redis lookups.

4. Relying Only on Blacklisting

Blacklisting adds server state, losing the stateless benefit of JWT. Use short TTL as primary and blacklisting as secondary.

5. Not Handling Redis Failures

If Redis is down, all tokens appear invalid (if checking) or valid (if not checking). Implement graceful degradation — allow valid tokens if Redis is unavailable.

Practice Questions

  1. What JWT claim is used for blacklisting?
  2. Why should blacklist entries have a TTL?
  3. When is blacklisting necessary despite short TTL?
  4. How does blacklisting affect JWT statelessness?
  5. What happens if Redis fails in a blacklist-based system?

Answers:

  1. The jti (JWT ID) claim — a unique identifier for each token.
  2. To prevent the blacklist from growing indefinitely. Once the token's natural expiry passes, the blacklist entry is no longer needed.
  3. For logout, immediate account suspension, or when a token is known to be compromised before its natural expiry.
  4. Blacklisting reintroduces server state, reducing (but not eliminating) the scaling benefits of stateless JWT.
  5. Implement fail-open (allow requests) or fail-closed (deny all) based on your security requirements. Fail-closed is safer.

Challenge: Build a blacklist system with Redis that supports immediate revocation, automatic TTL cleanup, and graceful degradation when Redis is unavailable.

FAQ

Does blacklisting make JWT stateful?

Partially. The token itself is stateless (still validated by signature), but the server must check the blacklist. It is hybrid stateless-stateful.

Can I blacklist without Redis?

Yes — use a database or in-memory set. But Redis is preferred for its TTL support and sub-millisecond lookups.

How large does the blacklist grow?

Only tokens that were explicitly revoked before expiry. With short TTL (15 min), most tokens expire naturally and never enter the blacklist.

Should I blacklist refresh tokens too?

Yes. Store blacklisted refresh tokens in the same Redis. This prevents revoked refresh tokens from obtaining new access tokens.

How do I handle user logout from all devices?

Maintain a user-level invalidation counter. Increment it on 'logout all'. Include the counter in the JWT. If the counter in the token is older than the current counter, reject.

Mini Project

Build a Flask API with JWT blacklisting using Redis. Implement login (issues JWT with jti), logout (adds jti to blacklist), and protected endpoints that check the blacklist before processing.

What's Next

Now learn about JKU and JWK — how to dynamically provide signing keys for JWT verification.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro