Routing by API Key vs JWT Claims
Platform teams operating multi-tenant APIs face a concrete decision before writing a single route rule: should the gateway resolve tenant identity by looking up an opaque API key in a credential store, or by verifying and decoding a signed JWT? The choice is not cosmetic — it sets the latency floor for every request, determines whether revocation is instant or eventually consistent, and dictates how many moving parts can fail independently at 3 AM.
Prerequisite Concepts
This page assumes familiarity with path and header-based routing, including how gateways match on X-API-Key or Authorization headers before dispatching to an upstream. It also assumes basic knowledge of the middleware chain model: plugins execute in declared order, so credential resolution must complete before header injection or rate limiting and throttling can run. For a broader capability matrix across gateway vendors, see Kong vs Tyk vs Envoy for Microservices.
How Each Mechanism Works at the Gateway
The diagram below shows the two resolution paths side by side, from incoming request to upstream dispatch.
API key routing is a synchronous, stateful lookup. The gateway reads X-API-Key from the request, queries a credential store (Redis, PostgreSQL, or DynamoDB), maps the opaque token to a consumer record, and injects normalized routing headers before forwarding. Revocation is instant — delete the record and the next request fails — but every request pays a network round-trip.
JWT claim routing is stateless at the edge. The gateway verifies the RS256 or ES256 signature using a locally cached public key set (JWKS), decodes the payload without any outbound call, and extracts routing directives from custom claims such as x-tenant-id or x-service-tier. Revocation requires waiting for token expiry or maintaining a server-side deny list, but requests clear the gateway without any I/O.
Side-by-Side Gateway Configuration
Kong 3.x — JWT Claims
Kong’s built-in jwt plugin validates the signature and expiry. A pre-function hook (Lua) then reads the decoded claim and injects it as a downstream header, because the request-transformer plugin cannot interpolate JWT payload fields directly.
# Kong 3.x declarative — config/kong.yaml
_format_version: "3.0"
services:
- name: tenant-api
url: http://upstream.internal:8080
routes:
- name: jwt-route
paths: ["/api/v1"]
strip_path: false
plugins:
- name: jwt
config:
claims_to_verify: [exp]
key_claim_name: iss
secret_is_base64: false
# Lua pre-function: copy decoded JWT claim into a header
- name: pre-function
config:
access:
- |
local claims = kong.ctx.shared.jwt_claims or {}
local tenant = claims["x-tenant-id"] or "unknown"
kong.service.request.set_header("X-Tenant-ID", tenant)
- name: rate-limiting
config:
minute: 1000
policy: redis
redis_host: redis.internal
redis_port: 6379
limit_by: header
header_name: X-Tenant-ID
Order matters: jwt runs first, writes decoded claims to kong.ctx.shared.jwt_claims, and then pre-function can safely read them before rate-limiting enforces the per-tenant bucket.
Kong 3.x — API Key
For API key routing, the key-auth plugin resolves the consumer and request-transformer injects the tenant header from the consumer’s tag or custom field.
# Kong 3.x declarative — API key variant
_format_version: "3.0"
consumers:
- username: tenant-acme
keyauth_credentials:
- key: "ak_acme_prod_abc123"
tags: ["tenant:acme", "tier:enterprise"]
services:
- name: tenant-api
url: http://upstream.internal:8080
routes:
- name: key-route
paths: ["/api/v1"]
plugins:
- name: key-auth
config:
key_names: ["X-API-Key"]
hide_credentials: true # strip key before forwarding
- name: request-transformer
config:
add:
headers:
- "X-Tenant-ID: acme" # static per-consumer; use pre-function for dynamic consumer tags
- name: rate-limiting
config:
minute: 500
policy: redis
redis_host: redis.internal
redis_port: 6379
limit_by: consumer
Set hide_credentials: true to strip X-API-Key before forwarding — never expose internal credential tokens to upstream services.
Apache APISIX 3.x — JWT Claims
APISIX’s jwt-auth plugin validates the token; proxy-rewrite can inject a static header, but for dynamic claim extraction you need the serverless-pre-function plugin or a custom Lua block.
# APISIX 3.x — routes/jwt-route.yaml
routes:
- id: jwt-tenant-route
uri: /api/v1/*
upstream:
type: roundrobin
nodes:
"upstream.internal:8080": 1
plugins:
jwt-auth:
header: Authorization
query: jwt
cookie: ""
serverless-pre-function:
phase: rewrite
functions:
- |
local jwt_token = ngx.req.get_headers()["x-jwt-tenant-id"]
if jwt_token then
ngx.req.set_header("X-Tenant-ID", jwt_token)
end
limit-req:
rate: 200
burst: 50
key_type: var
key: http_x_tenant_id
rejected_code: 429
Apache APISIX 3.x — API Key
# APISIX 3.x — routes/key-route.yaml
routes:
- id: key-tenant-route
uri: /api/v1/*
upstream:
type: roundrobin
nodes:
"upstream.internal:8080": 1
plugins:
key-auth:
header: X-API-Key
proxy-rewrite:
headers:
set:
X-Tenant-ID: "$consumer_name" # APISIX expands consumer variable
limit-req:
rate: 100
burst: 20
key_type: var
key: http_x_tenant_id
rejected_code: 429
Decision Matrix
| Criterion | API Key | JWT Claims |
|---|---|---|
| Latency per request | 3–15 ms (I/O) | 0.5–2 ms (CPU) |
| Revocation speed | Instant (delete record) | At token expiry (or deny list) |
| Horizontal scalability | Requires shared credential store | Fully stateless — scales with compute |
| Credential rotation | Rotate in store; no client redeploy | Re-issue token; old token valid until exp |
| Offline / edge execution | Fails without store access | Works from cached JWKS |
| Auditability | Per-request lookup logged centrally | Decode at edge; audit via token jti claim |
| Implementation complexity | Low (one lookup) | Medium (JWKS management, claim mapping) |
| Multi-tenant isolation | Centralized, strict | Cryptographic boundaries |
Choose API keys when you need instant, auditable revocation and operate internal service-to-service traffic where latency headroom is generous. Choose JWT claims when edge latency must stay below 5 ms, consumers are external or geographically distributed, or tenant isolation relies on cryptographic rather than database-backed boundaries. For accurate per-tenant enforcement, both paths must normalize to the same header (X-Tenant-ID) before the rate-limit plugin reads it.
Failure Modes and Gotchas
JWT — JWKS Cache Invalidation During Key Rotation
If the identity provider rotates signing keys faster than the gateway’s JWKS cache refresh interval, requests fail with 401 Unauthorized. The gateway holds the old public key and cannot verify tokens signed with the new one.
Mitigate by:
- Setting the JWKS polling interval below 60 seconds (default is often 15 minutes in Kong and NGINX).
- Configuring the identity provider to publish both the old and new key in the JWKS response for at least one full rotation window.
- Enabling background key prefetch so the cache refreshes before the old key expires.
Diagnose rotation mismatches:
# Test a fresh token against the gateway
curl -sS -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $(get-token)" \
https://gateway.example.com/api/v1/health
# Expected 200; if 401, check gateway JWKS cache age
API Key — Credential Store Outage and Cache Stampedes
When the Redis credential cache misses and the primary datastore is slow or down, concurrent requests for uncached keys fan out into simultaneous lookup calls, overwhelming the database.
Mitigate by:
- Deploying a Redis-backed credential cache with a 60-second TTL and jittered eviction windows to prevent synchronized expiry.
- Attaching a circuit breaker to the lookup stage — open the circuit when backend latency exceeds 50 ms or error rate crosses 5%, and return
503 Service Unavailable. The security boundaries and zero-trust section covers resilience patterns including circuit-breaker configuration at the gateway edge. - Using request coalescing: hold duplicate in-flight lookups for the same uncached key and return the single result to all waiters.
API Key — Credential Leakage in Forwarded Headers
If hide_credentials is not set, the raw X-API-Key value propagates to every upstream service in the header set. Any upstream log or debug endpoint then leaks a live credential.
Always set hide_credentials: true (Kong) or the equivalent proxy-rewrite header-removal step (APISIX) to strip the raw key before forwarding.
JWT — Clock Skew Causing Premature Expiry Rejection
A mismatch between the gateway’s system clock and the token issuer’s clock causes exp validation to reject tokens that are technically still valid. Allow a configurable clock skew tolerance (leeway in Kong’s JWT plugin, typically 5–30 seconds) and ensure both systems sync to the same NTP source.
Validation Checklist
-
hide_credentials: trueset on key-auth plugin; raw API key does not appear in upstream request headers. - JWKS polling interval is below 60 seconds; identity provider publishes overlapping keys during rotation.
- Both auth paths normalize to the same downstream header (
X-Tenant-ID) before rate-limit and routing plugins read it. - Redis credential cache TTL aligns with upstream key rotation window; jitter is applied to eviction.
- Circuit breaker is wired to the credential lookup stage with latency and error-rate thresholds.
- Clock skew leeway is configured on JWT validation (5–30 seconds); all systems use NTP sync.
-
expclaim is included inclaims_to_verify; tokens without expiry are rejected. - Integration test covers the fallback path: JWT absent → key-auth succeeds; key-auth absent → JWT succeeds.
- Gateway access logs confirm credential headers are stripped and
X-Tenant-IDis present on forwarded requests.
FAQ
Can a gateway accept both API keys and JWTs on the same route?
Yes. Configure the JWT plugin first, then fall through to key-auth when the Authorization header is absent. Both paths must normalize to the same downstream header (e.g., X-Tenant-ID) so upstream services remain auth-agnostic during migration.
How do I prevent API key cache stampedes during a credential database outage?
Attach a circuit breaker to the credential lookup stage. When backend latency exceeds 50 ms or error rate crosses 5%, open the circuit and reject with 503. Use request coalescing to prevent multiple in-flight lookups for the same uncached key.
Why does JWT validation still add latency if it avoids network round-trips?
RS256/ES256 signature verification is CPU-bound — typically 0.5–2 ms per request on a single core. At high concurrency, CPU saturation becomes the bottleneck. Distribute verification across CPU-optimized edge nodes and keep the JWKS cache warm to eliminate cold-start overhead.
Parent: Path & Header-Based Routing
Related
- Authentication Proxying & Token Validation — middleware-layer token inspection patterns that complement gateway-level routing decisions
- Implementing JWT Validation in Kong Plugins — deep-dive on Kong’s JWT plugin configuration and custom claim handling
- Multi-Tenant Routing Strategies — broader tenant isolation models that determine which auth mechanism fits your tenancy architecture