Token Introspection vs Local JWT Validation
Local JWT validation is fast, self-contained and slightly wrong: it accepts a token that was revoked five minutes ago. Introspection is correct and puts a network call on every request. Most production systems need both, applied to different routes, and the interesting work is deciding where the boundary sits and how to keep the correct-but-slow path from becoming a dependency that takes the API down with it.
Prerequisite concepts
This assumes the token lifecycle and key-rotation model in authentication proxying and token validation, and the zero-trust framing in security boundaries and zero trust.
What each one actually checks
Splitting by route, not by system
The useful boundary is per route, chosen by what an accepted-but-revoked token would let someone do.
# Kong 3.x — local validation everywhere, introspection on the routes that matter
services:
- name: catalogue
url: http://catalogue.internal:8080
routes: [{ name: catalogue-read, paths: ["/v2/catalogue"], methods: ["GET"] }]
plugins:
- name: jwt # local: signature + exp, no network
- name: payments
url: http://payments.internal:8080
routes: [{ name: payments-write, paths: ["/v2/payments"], methods: ["POST"] }]
plugins:
- name: oauth2-introspection
config:
introspection_url: https://idp.example.com/oauth2/introspect
ttl: 30 # cache the positive answer briefly
timeout: 800 # ms — bound the dependency
run_on_preflight: false
Reading a product catalogue with a token revoked two minutes ago is a non-event. Moving money with one is not. That asymmetry is the whole design: pay the round trip where the consequences justify it.
Caching, and the two things it must not do
A cache in front of introspection is mandatory at any real request rate, and it has two failure modes worth designing against explicitly.
Decision matrix
| Route characteristic | Validation |
|---|---|
| Read-only, non-sensitive | local |
| Writes money or grants access | introspection, short cache |
| Called at very high rate | local, with short token lifetimes instead |
| Administrative or privileged | introspection, no cache |
| Third-party opaque tokens | introspection — there is no alternative |
| Internal service-to-service | workload identity, neither |
Short token lifetimes are the underrated third option: a five-minute token bounds the revocation window without any per-request network call, and for many APIs it is a better trade than either extreme.
Gotchas and failure signals
Introspection is an upstream dependency with a timeout, a retry policy and a failure mode. Treat it as one: bound it, monitor it, and know what happens when it is slow rather than down — a two-second introspection call on a route with a one-second budget fails every request while the identity provider reports itself healthy.
Caching a negative answer protects you from a client retrying an invalid token in a loop and turning your gateway into a denial-of-service amplifier against your own identity provider.
Introspection responses can carry claims local validation cannot see, such as current scope after a downgrade. If you route or authorise on those, they are only correct on the introspected path.
A gateway that introspects on every request and caches nothing will be the reason the identity provider falls over, and the incident will look like an identity provider problem.
Refresh tokens change the calculation
Short access tokens are only tolerable if renewing them is cheap and invisible, which is what a refresh token provides: the client holds a long-lived credential it presents only to the identity provider, and exchanges it for a short-lived access token the gateway can validate locally.
That arrangement moves the revocation problem to a place better suited to solve it. Revoking the refresh token stops renewal, so the blast radius is bounded by the access-token lifetime rather than by the session length, and the check happens at the identity provider rather than on every API request. From the gateway’s point of view nothing changes at all — it keeps validating short-lived tokens locally — while the security property improves substantially.
The failure mode to watch for is a client that treats renewal as exceptional rather than routine. A client that only refreshes after receiving a 401 will produce a burst of renewals whenever a large cohort of tokens expires together, and that burst lands on the identity provider rather than on the API. Stagger token lifetimes slightly, or have clients renew proactively at a random point inside the last third of the lifetime.
Validation
- Route-level split documented, with the reason each route sits where it does
- Introspection has an explicit timeout well inside the route’s latency budget
- Positive and negative cache lifetimes set separately and deliberately
- Failure mode chosen, exercised in a game day, and exported as a metric
- Cache keys are hashes, never raw tokens
- Token lifetime considered as an alternative before adding a network call
FAQ
Why can local validation not detect a revoked token?
Because revocation is state held by the issuer that came into existence after the token was signed, and a signature cannot encode a fact from the future. Local validation can only tell you the token was issued by someone you trust, to the audience you expect, and has not yet expired. Everything else requires asking the issuer.
How do I decide which routes get introspection?
By what an accepted-but-revoked token would let someone do. Reading a public catalogue with a token revoked two minutes ago is a non-event; moving money with one is not. Put introspection on the routes where the consequences justify a round trip, and use local validation with short token lifetimes everywhere else.
How long should I cache an introspection result?
Cache a positive answer for tens of seconds, which bounds how long a revoked token keeps working, and a negative answer for minutes, since a token that was inactive rarely becomes active again. Caching the negative result also stops a client retrying an invalid token in a loop from turning your gateway into an amplifier against your own identity provider.
What should happen when the identity provider is unreachable?
There is no universally correct answer, which is exactly why it must be chosen deliberately rather than inherited from a default. Failing open keeps traffic flowing and honours revocations late; failing closed protects the resource and converts an identity provider outage into an API outage. Whichever you pick, export a metric, because an introspection layer that silently stopped introspecting looks identical to one that is working.
Parent: Security Boundaries & Zero Trust
Related
- Security Boundaries & Zero Trust — the trust model these two mechanisms serve.
- Authentication Proxying & Token Validation — token lifecycle, key rotation and where validation sits in the chain.
- Rotating JWT Signing Keys Without Downtime — the key-set refresh that local validation depends on.