JWK cache
Every time sign(), verify(), encrypt(), or decrypt() touches a JWK, unjwt must call crypto.subtle.importKey to produce a CryptoKey. That import is not free — especially for RSA keys, where parsing modulus and exponent takes a few milliseconds.
To avoid paying that cost on every call, unjwt keeps a small per-JWK cache. By default, it's a WeakMap keyed by the JWK object reference; no tuning required.
import { configureJWKCache, clearJWKCache, WeakMapJWKCache } from "unjwt/jwk";
How it works — the default
WeakMapJWKCache stores WeakMap<JWK, Record<string, CryptoKey>>:
- Outer key — the JWK object reference itself.
- Inner key — the algorithm string passed to
importKey(e.g."HS256","RS256"). - Value — the resulting
CryptoKey.
Because the outer map is a WeakMap, the cache entry is garbage-collected automatically when the JWK object becomes unreachable. No manual invalidation needed.
// First call: imports, caches
await verify(token, myJwk);
// Second call with SAME object: cache hit, skip import
await verify(token2, myJwk);
// With a copy: CACHE MISS — different object reference
await verify(token3, { ...myJwk }); // re-imports
{ ...jwk }) or a JSON.parse of a stringified JWK produces a new object and misses the cache. For predictable hits, keep one JWK object per key and reuse the reference.configureJWKCache
Replace or disable the active cache:
import { configureJWKCache } from "unjwt/jwk";
// Custom kid-keyed cache with LRU semantics
const map = new Map<string, CryptoKey>();
configureJWKCache({
get: (jwk, alg) => map.get(`${jwk.kid}:${alg}`),
set: (jwk, alg, key) => map.set(`${jwk.kid}:${alg}`, key),
});
// Disable caching entirely
configureJWKCache(false);
The adapter interface
Any object matching this shape is accepted:
interface JWKCacheAdapter {
get(jwk: JWK, alg: string): CryptoKey | undefined;
set(jwk: JWK, alg: string, key: CryptoKey): void;
}
No TTL, no delete — cache invalidation is up to the adapter. The default implementation leans on JavaScript's garbage collector via WeakMap; an LRU or Redis-backed version would need its own eviction strategy.
When to replace
- You parse JWKs from strings on every request — the default
WeakMapcache misses, so a kid-keyed map gives you real hits again. - Multi-tenant environments — an LRU bound prevents unbounded memory growth in long-lived processes.
- Distributed workers — a shared (Redis, etc.) cache lets you amortize imports across a pool.
When to disable
- Memory-constrained environments where the cache outweighs the wins.
- Tests, especially when you want each test's imports to be observable (but see
clearJWKCachebelow — usually sufficient).
clearJWKCache
import { clearJWKCache } from "unjwt/jwk";
clearJWKCache();
Resets the cache to a fresh WeakMapJWKCache. Mostly used in tests to ensure no cross-test key memoization. Call it in beforeEach if your tests re-use JWK objects across runs.
WeakMapJWKCache
The default implementation, exported for reference:
export class WeakMapJWKCache implements JWKCacheAdapter {
private cache = new WeakMap<JWK, Record<string, CryptoKey>>();
get(jwk: JWK, alg: string): CryptoKey | undefined {
/* ... */
}
set(jwk: JWK, alg: string, key: CryptoKey): void {
/* ... */
}
}
Internally it uses Record<string, CryptoKey> instead of Map<string, CryptoKey> for the per-JWK bucket — plain objects are slightly faster than Map for the typical 1–2 algorithm entries per key (V8 hidden-class optimization).
See also
- Importing & exporting →
- JWK Sets → — complementary for managing many keys.