# 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.

```ts
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.

```ts
// 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
```

<warning>

Cache hits require the **same object variable** to be passed. A spread copy (`{ ...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.

</warning>

## `configureJWKCache`

Replace or disable the active cache:

```ts [custom.ts]
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:

```ts
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 `WeakMap` cache 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 [`clearJWKCache`](#clearjwkcache) below — usually sufficient).

## `clearJWKCache`

```ts
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:

```ts
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/import-export)
- [JWK Sets →](/jwk/jwk-sets) — complementary for managing many keys.
