Token Revocation Service Low-Level Design: Blocklist, JTI Tracking, and Fast Invalidation

Problem Scope and Requirements

JWTs are stateless by design — once issued, they are valid until expiry. A Token Revocation Service provides the missing stateful layer: a fast blocklist that allows tokens to be invalidated before their natural expiry, enabling logout, credential rotation, and breach response.

Functional Requirements

  • Revoke individual tokens by JWT ID (JTI claim) within milliseconds of the revocation call.
  • Support bulk revocation of all tokens for a given user (e.g., on password change).
  • Token validation must check the blocklist as part of every authenticated request.
  • Automatically purge revocation records after the token's natural expiry to bound storage growth.
  • Support revocation reason codes for audit purposes.

Non-Functional Requirements

  • Blocklist check under 0.5 ms P99 — it is on the hot path of every API call.
  • Revocation propagated to all validation nodes within 1 second.
  • Handle 200 million active tokens with 1 million revocations per day.

Core Data Model

JTI Blocklist Entry (Redis)

Key:   "jti:{jti}"
Value: {
    user_id:    string
    revoked_at: int64
    reason:     enum { LOGOUT, PASSWORD_CHANGE, COMPROMISED, ADMIN_REVOKE }
    revoked_by: string    // service or admin ID
}
TTL:   token_exp - now()   // expires when the token would have naturally expired

User-Level Revocation (for bulk invalidation)

Key:   "user_rev:{user_id}"
Value: int64   // Unix timestamp: all tokens issued before this time are invalid
TTL:   max token lifetime (e.g., 24 hours)

Revocation Audit Log (append-only DB)

RevocationEvent {
    event_id:    UUID
    jti:         string         // nullable for user-level revocations
    user_id:     string
    reason:      string
    revoked_at:  int64
    revoked_by:  string
    token_exp:   int64
}

JTI-Based Individual Revocation

Every JWT issued by the authorization server must include a jti claim containing a unique identifier (UUID v4 or a CSPRNG-generated 128-bit value base64url-encoded). The JTI is stored in Redis with a TTL equal to the token's remaining lifetime. On each token validation:

  1. Verify JWT signature and standard claims (exp, nbf, iss, aud).
  2. Extract the jti claim.
  3. Execute Redis GET "jti:{jti}". If the key exists, the token is revoked — return 401.
  4. Check user-level revocation: GET "user_rev:{user_id}". If the value exists and is greater than the token's iat (issued-at), the token is revoked.

Both checks are pipelined in a single Redis round-trip using MGET to stay under 0.5 ms.

User-Level Bulk Revocation

When a user changes their password or a breach is detected, set "user_rev:{user_id}" to the current timestamp with a TTL equal to the maximum token lifetime. This single Redis write effectively invalidates all tokens issued before this moment without enumerating them. The JTI check still runs first — if somehow a specific token has already been individually revoked, the individual entry provides the reason code for audit.

Clearing the user-level revocation key (after TTL) must not re-validate old tokens. Since the key expires after the maximum token lifetime, any token issued before the revocation timestamp will have also naturally expired by then. No old tokens can slip through.

Sub-Millisecond Check Optimization

Redis GET with network RTT on a local subnet is typically 0.1–0.3 ms. To cut this further:

  • Local Bloom filter: Each validation node maintains an in-memory Bloom filter of revoked JTIs, synced from Redis on startup and updated via pub/sub on each new revocation. A Bloom filter check is nanoseconds. False positives (rare valid tokens blocked) are acceptable if the false positive rate is configured below 0.001%. False negatives are impossible — if the filter says “not revoked,” it is definitely not revoked.
  • Pub/sub propagation: When a revocation is written to Redis, publish a message on a revocations channel. All validation nodes subscribed to this channel update their local Bloom filters immediately, achieving sub-second propagation without polling.
  • Bloom filter sizing: For 200 million active tokens and 0.001% false positive rate, a Bloom filter requires approximately 4.3 GB of memory (using the standard formula). This may be too large for in-process use; alternatives include Cuckoo filters (more memory-efficient, support deletion) or a tiered approach with a smaller filter for recent revocations only.

TTL and Storage Management

JTI blocklist entries are given a TTL equal to the token's remaining validity period at time of revocation. Redis automatically evicts them. No background cleanup job is needed for individual JTIs. The user_rev keys are also TTL-managed. The append-only audit database retains records for compliance purposes (typically 90 days to 7 years) and is separate from the hot Redis path.

To bound Redis memory under revocation spikes: configure Redis with maxmemory-policy allkeys-lru only if you are willing to accept potential loss of very recent revocations under extreme memory pressure. For security-critical deployments, use noeviction and alert on memory thresholds instead — availability of the revocation check matters less than correctness.

API Design

POST   /revocations/token          — revoke by JTI { jti, exp, user_id, reason }
POST   /revocations/user           — bulk revoke all tokens for user_id
GET    /revocations/check/{jti}    — is this JTI revoked? (for debugging/admin only)
GET    /revocations?user_id={uid}  — list revocation events for a user (audit)
DELETE /revocations/{jti}          — un-revoke (rare; requires admin privilege and audit log entry)

Scalability Considerations

  • Redis cluster sharding: Shard on JTI prefix. Since JTIs are random, sharding is automatically even. User_rev keys shard on user_id. Cross-slot pipelining of a JTI check and a user_rev check requires routing to two shards; handle this with async parallel requests to both shards and combine results.
  • Multi-region: Replicate revocations to all regions via Kafka or a geo-distributed Redis (e.g., Redis Enterprise Active-Active). Revocation is an append-only operation, so eventual consistency (within 1–2 seconds) is acceptable. A token valid for 15 minutes cannot be immediately used in another region for longer than the replication lag.
  • Token lifetime reduction: The best architectural mitigation for revocation latency is to issue short-lived tokens (5–15 minutes) paired with refresh tokens. Short-lived tokens reduce the window in which a revoked token can be abused even without blocklist checks.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does a JTI blocklist in Redis enable token revocation?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each JWT carries a unique JWT ID (JTI). On revocation the JTI is written to a Redis SET with a TTL matching the token's remaining lifetime. Every resource server checks the JTI on each request; a cache hit means the token is revoked and the request is rejected with 401.”
}
},
{
“@type”: “Question”,
“name”: “How do you propagate revocations across multiple data centers with pub/sub?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“@type”: “Answer”,
“text”: “The auth service publishes a revocation event to a Kafka topic (or Redis Pub/Sub channel) keyed by user ID. Every data center's cache layer subscribes and writes the JTI into its local Redis instance, achieving cross-region consistency within milliseconds without a synchronous round-trip.”
}
},
{
“@type”: “Question”,
“name”: “When is a Bloom filter a good optimization for the JTI blocklist?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A Bloom filter sits in front of Redis as an in-process pre-check. For the vast majority of requests—where the token is valid—it returns a definite negative in O(k) hash operations without a network call. False positives fall through to Redis for confirmation; the false-negative rate is zero, so no valid revocation is ever missed.”
}
},
{
“@type”: “Question”,
“name”: “Why must the Redis blocklist instance use the noeviction policy?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “With any eviction policy Redis can silently drop JTI entries under memory pressure, allowing revoked tokens to pass validation again. Setting maxmemory-policy to noeviction makes Redis return errors instead of evicting data, forcing the system to handle capacity explicitly rather than creating a silent security hole.”
}
}
]
}

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

Scroll to Top