vext-first response caching toolkit for Node.js services.
response-cache-kit is part of the vext ecosystem. Its first acceptance target
is replacing vext response caching, while keeping the core independent from any
specific web framework. Express, Fastify, Hono, native HTTP, and future vext
adapters should reuse the same helpers instead of copying cache plumbing.
Chinese documentation: docs/README.zh-CN.md
- Install
- Quick Start
- Complete Configuration Example
- Configuration
- Per-Route Overrides
- Tags And Invalidation
- API Reference
- Framework Integration Examples
- Defaults
- Concurrent Expiry Protection
- vext Migration Notes
- Future Batches
- Scripts
- Troubleshooting
- License
npm install response-cache-kitimport {
createResponseCache,
createResponseCacheWritePayload,
} from "response-cache-kit";
const cache = createResponseCache({ ttl: 2_000 });
const result = await cache.handle(
{
method: "GET",
url: "/products/42",
headers: { "accept-language": "en-US" },
},
async () => {
return {
status: 200,
headers: { "content-type": "application/json" },
body: { id: 42, name: "Keyboard" },
};
}
);
console.log(result.metadata.state); // miss, hit, deduped, bypass, or error
console.log(
createResponseCacheWritePayload(result, { cacheControl: true }).headers
); // includes X-Cache and Cache-ControlThe package caches response snapshots: status, headers, body, TTL, and cache metadata. A router or framework adapter decides where to call it.
Public responses do not need partitionKey. Responses that depend on a user,
tenant, session, or permission scope should pass partitionKey when calling
cache.handle().
This example shows the common production shape: a global cache instance, header variants for language and tenant region, per-request user or tenant isolation, and cache metadata written back to the response.
import {
createResponseCache,
createResponseCacheWritePayload,
normalizeResponseCacheRequest,
} from "response-cache-kit";
const responseCache = createResponseCache({
ttl: 30_000,
namespace: "products-api",
vary: ["accept-language", "x-tenant-region"],
cacheHub: {
maxEntries: 5000,
cleanupInterval: 30_000,
enableStats: true,
},
});
async function handleProductRequest(req, res) {
const tenantId = req.user?.tenantId;
const userId = req.user?.id;
const partitionKey =
tenantId && userId ? `tenant:${tenantId}:user:${userId}` : undefined;
const result = await responseCache.handle(
normalizeResponseCacheRequest({
method: req.method,
url: req.originalUrl ?? req.url,
headers: req.headers,
...(partitionKey ? { partitionKey } : {}),
}),
async () => ({
status: 200,
headers: { "content-type": "application/json" },
body: await loadProduct(req.params.id),
}),
{ tags: ["products"] }
);
const payload = createResponseCacheWritePayload(result, {
cacheControl: true,
});
for (const [name, value] of Object.entries(payload.headers)) {
res.setHeader(name, value);
}
res.status(payload.status).send(payload.body);
}Use partitionKey only for identity-dependent responses. Do not use raw access
tokens as partition keys; use stable business identifiers such as user, tenant,
organization, or session IDs.
| Option | Type | Default | Description |
|---|---|---|---|
cacheHub |
ResponseCacheHubOptions |
{} |
Options for the internal cache-hub store. Omit mode for Memory, or set mode: "redis" / "multi-level" explicitly. defaultTtl is not exposed because response TTL is controlled by ttl. |
ttl |
number |
60000 |
Cache TTL in milliseconds. ttl <= 0 bypasses caching. |
namespace |
string |
"response-cache" |
Prefix used when building cache keys. Useful when sharing one store across modules. |
vary |
readonly string[] | "*" |
[] |
Header names that separate cache entries when those headers change the response. Use "*" only when every request header intentionally participates in the key. |
tags |
readonly string[] |
[] |
Tags written with stored responses. Use cache.invalidateTag(tag) after business data changes. |
cacheableMethods |
readonly string[] |
["GET", "HEAD"] |
Methods eligible for caching. Values are normalized to uppercase. |
cacheableStatuses |
readonly number[] |
[200, 203, 204, 206, 300, 301, 404, 410] |
Response statuses eligible for storage. |
allowAuthorizationCache |
boolean |
false |
Allows caching requests with Authorization. Prefer partitionKey for user/tenant separation. |
now |
() => number |
Date.now |
Clock function, mainly for tests and deterministic benchmarks. |
keyBuilder |
ResponseCacheKeyBuilder |
built-in SHA-256 key builder | Advanced cache key builder. Receives the request plus namespace, resolved vary, and varyAllHeaders. |
cacheHub is intentionally a configuration object, not a way to pass your own
store instance. response-cache-kit creates the underlying cache-hub store
internally.
import { createResponseCache } from "response-cache-kit";
const cache = createResponseCache({
ttl: 10_000,
namespace: "api",
vary: ["accept-language", "accept-encoding"],
cacheHub: {
maxEntries: 5000,
maxMemory: 64 * 1024 * 1024,
cleanupInterval: 30_000,
enableStats: true,
},
});Memory cacheHub fields:
| Field | Type | Default from cache-hub |
Description |
|---|---|---|---|
maxEntries |
number |
10000 |
Maximum number of entries kept by the internal memory store. |
maxMemory |
number |
0 |
Approximate memory limit in bytes. 0 means unlimited. |
enableStats |
boolean |
true |
Enables cache-hub store statistics. |
cleanupInterval |
number |
0 |
Periodic cleanup interval in milliseconds. 0 means lazy cleanup only. |
enabled |
boolean |
true |
Temporarily disables reads and writes in the internal store when set to false. |
defaultTtl and enableTags are not exposed in cacheHub. Response TTL belongs
to response-cache-kit; tag indexes are enabled internally so response tags and
invalidateTag() work without user store wiring.
Redis and multi-level modes are opt-in. They still use cache-hub; there is no
custom external store injection.
const redisCache = createResponseCache({
ttl: 10_000,
namespace: "api",
cacheHub: {
mode: "redis",
url: "redis://localhost:6379",
metaKeyPrefix: "api:response-cache",
scanCount: 200,
deleteCommand: "unlink",
lease: {
ttl: 1_000,
waitForOwner: 1_200,
pollInterval: 10,
onTimeout: "fetch",
},
},
});
const multiLevelCache = createResponseCache({
ttl: 10_000,
namespace: "api",
cacheHub: {
mode: "multi-level",
memory: { maxEntries: 5000 },
redis: {
url: "redis://localhost:6379",
metaKeyPrefix: "api:response-cache",
},
writePolicy: "both",
backfillOnRemoteHit: true,
remoteTimeout: 50,
lease: true,
},
});Redis mode fields:
| Field | Type | Default | Description |
|---|---|---|---|
mode |
"redis" |
required | Enables the cache-hub Redis adapter. |
url |
string |
"redis://localhost:6379" |
Redis URL used when no client is provided. URL mode uses cache-hub's optional ioredis peer. |
client |
object |
none | Existing Redis-like client. Lifecycle remains owned by the caller. |
metaKeyPrefix |
string |
cache-hub default | Prefix for tag metadata keys. |
scanCount |
number |
cache-hub default | SCAN batch size for pattern/tag operations. |
deleteCommand |
"del" | "unlink" |
"del" |
Redis delete command used by cache-hub. |
lease |
boolean | ResponseCacheHubLeaseOptions |
false |
Enables Redis-backed cross-process refresh coordination. |
distributed |
boolean | ResponseCacheHubDistributedOptions |
false |
Enables cache-hub distributed tag invalidation. |
Multi-level mode fields:
| Field | Type | Default | Description |
|---|---|---|---|
mode |
"multi-level" |
required | Uses local Memory as L1 and Redis as L2. |
memory |
ResponseCacheHubMemoryOptions |
{} |
L1 Memory options. |
redis |
ResponseCacheHubRedisTargetOptions |
{} |
L2 Redis target and metadata options. |
writePolicy |
"both" | "local-first-async-remote" |
cache-hub default | Write-through behavior. |
backfillOnRemoteHit |
boolean |
cache-hub default | Whether L2 hits should refill L1. |
remoteTimeout |
number |
cache-hub default | L2 read timeout in milliseconds. |
remoteInvalidationErrors |
"ignore" | "throw" |
cache-hub default | Whether L2 tag invalidation failures throw. |
lease |
boolean | ResponseCacheHubLeaseOptions |
false |
Uses the Redis layer for cross-process lease coordination. |
distributed |
boolean | ResponseCacheHubDistributedOptions |
false |
Broadcasts tag invalidation across instances. |
Lease fields:
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true when object/true is provided |
Set false to disable a prepared lease config. |
ttl |
number |
derived from response TTL, capped for refresh windows | Lease TTL in milliseconds. |
waitForOwner |
number |
leaseTtl + 25 |
How long a non-owner waits for the owner to fill cache. |
pollInterval |
number |
10 |
Cache polling interval while waiting. |
onTimeout |
"fetch" | "throw" |
"fetch" |
Whether to run origin or throw when no owner fills cache in time. |
keyPrefix |
string |
cache-hub default | Redis lease key prefix. |
ownerId |
string |
generated by cache-hub | Stable owner prefix for lease tokens. |
Distributed fields:
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true when object/true is provided |
Set false to disable a prepared distributed config. |
redisUrl |
string |
cache-hub default | Redis URL for pub/sub when no redis object is provided. |
redis |
object |
none | Existing Redis-like pub connection. |
channel |
string |
cache-hub default | Pub/sub invalidation channel. |
instanceId |
string |
generated by cache-hub | Instance identifier used to ignore self messages. |
When Redis URL mode or distributed invalidation is enabled, the consuming
application must satisfy cache-hub's optional Redis peer. Default Memory mode
does not load or require Redis. Call await cache.close?.() during application
shutdown when using Redis, MultiLevel, or distributed invalidation.
ttl controls how long a stored response stays fresh. Use shorter values for
fast-changing data and longer values for slow-changing catalog or reference
data. ttl <= 0 bypasses storage, which is useful for temporarily disabling
response caching without changing the call site.
namespace is the prefix used in generated cache keys. Change it when multiple
services, modules, or environments share the same underlying cache process.
Examples: "products-api", "admin-api", or "staging-products-api".
vary accepts request header names. Header names are matched
case-insensitively. Only add headers that truly change the response body or
headers.
Good candidates:
accept-languagewhen localized responses differ by language.accept-encodingwhen the origin returns different encoded bodies.x-tenant-regionor another low-cardinality business header that changes the response.- A client version header when different app versions receive different payloads.
Avoid:
authorizationandcookie; usepartitionKeyfor identity isolation.- Trace IDs, request IDs, timestamps, nonce headers, or any value that changes on every request.
- Very high-cardinality headers that would create a separate cache entry for nearly every request.
If a response changes by language and vary does not include
accept-language, different languages may share the same cached response.
Use vary: "*" only when all request headers are part of your response
contract. It is convenient for strict internal adapters, but dangerous for public
traffic because request IDs, trace IDs, dates, cookies, or authorization-like
headers can produce a near-zero hit rate or leak sensitive key material through a
custom key builder. Prefer an explicit allowlist for normal applications.
partitionKey is not an HTTP header and is not read automatically. It is a
string you pass in cache.handle() to separate cached responses by user, tenant,
organization, session, or permission scope.
Common shapes:
// Public response: no partitionKey.
{ method: "GET", url: "/products", headers: req.headers }
// User-specific response.
{ method: "GET", url: "/me", headers: req.headers, partitionKey: `user:${userId}` }
// Tenant-specific response.
{ method: "GET", url: "/settings", headers: req.headers, partitionKey: `tenant:${tenantId}` }
// Multi-tenant user-specific response.
{
method: "GET",
url: "/dashboard",
headers: req.headers,
partitionKey: `tenant:${tenantId}:user:${userId}`,
}Requests with Authorization are bypassed by default unless you provide
partitionKey or explicitly enable allowAuthorizationCache. Prefer
partitionKey; it keeps authenticated responses isolated without putting raw
tokens into cache keys.
The default false is a safety default. It prevents a response for one
authenticated caller from being reused by another caller by accident.
Set allowAuthorizationCache: true only when you are certain the response is
safe to share for the key you build. For most user or tenant responses, pass a
partitionKey instead.
cacheableMethods defaults to ["GET", "HEAD"]. Only add methods such as
POST when the endpoint is idempotent or the business flow explicitly treats
the response as cacheable.
cacheableStatuses defaults to common cacheable success, redirect, and negative
lookup statuses. Caching 404 or 410 can reduce repeated misses for missing
resources. Avoid caching server errors unless the behavior is intentionally
designed.
cacheHub configures the internal cache-hub store. It does not accept an
external store instance.
maxEntries: cap the number of cached responses.maxMemory: approximate memory limit in bytes;0means no explicit limit.enableStats: keep cache-hub store statistics available for diagnostics.cleanupInterval: periodically remove expired entries;0means lazy cleanup.enabled: whentrue, the store reads and writes normally. Whenfalse, the underlying store is disabled, so requests still run throughcache.handle()but cannot build useful hits. Use it for local debugging or temporary cache shutdowns, not as a long-term production setting.mode: "redis": stores response snapshots in cache-hub's Redis adapter.mode: "multi-level": uses cache-hub's L1 Memory + L2 Redis cache.lease: optional Redis-backed cross-process refresh coordination. Same-process single-flight remains enabled in every mode.distributed: optional cache-hub Pub/Sub invalidation for tags across instances.
keyBuilder is an escape hatch for teams that already have a shared cache key
format. If you provide it, make sure your key includes every piece of data that
can change the response: URL, normalized query, selected vary headers, and
partitionKey.
Tags are a framework-neutral way to invalidate many response entries after a business change. Tags can be configured globally and per call; per-call tags are added to global tags.
const cache = createResponseCache({
ttl: 60_000,
tags: ["catalog"],
});
await cache.handle(
{ method: "GET", url: "/products/42" },
loadProductResponse,
{ tags: ["product:42"] }
);
await cache.invalidateTag("product:42");Use readable keys when you want targeted deletion:
const key = cache.makeKey({ method: "GET", url: "/products/42" });
await cache.delete(key);cache.stats() exposes the internal cache-hub counters, and
cache.getRemainingTtl(key) returns the remaining TTL in milliseconds,
null for a non-expiring key, or undefined when the key is missing or the
store cannot report TTL.
Global options apply to most routes. Per-route overrides are for endpoints that
need different behavior, such as a shorter TTL or a cacheable POST response.
cacheHub cannot be overridden per route because it configures the lifecycle of
the internal store and must be chosen when the cache instance is created.
await cache.handle(
{ method: "POST", url: "/reports" },
createReport,
{
ttl: 5_000,
cacheableMethods: ["POST"],
cacheableStatuses: [202],
}
);Runs the response cache flow and returns a normalized response result.
request fields:
| Field | Type | Required | Description |
|---|---|---|---|
url |
string |
Yes | URL or path used in the cache key. Query parameters are normalized. |
method |
string |
No | Defaults to GET. |
headers |
HeadersLike |
No | Plain object, iterable entries, or Web Headers-like object. |
partitionKey |
string |
No | Adds user/tenant/session separation for authenticated responses. |
origin returns:
| Field | Type | Required | Description |
|---|---|---|---|
body |
unknown |
Yes | Response body to return and optionally cache. |
status |
number |
No | Defaults to 200. |
headers |
HeadersLike |
No | Response headers. Hop-by-hop headers are filtered before storage. |
handle() returns:
| Field | Type | Description |
|---|---|---|
status |
number |
Normalized response status. |
headers |
Record<string, string> |
Normalized response headers. |
body |
unknown |
Response body. |
metadata |
ResponseCacheMetadata |
Cache state, key, reason, age, TTL, storage and dedupe flags. |
Metadata states:
| State | Meaning |
|---|---|
miss |
No usable cached snapshot; origin ran. |
hit |
Cached snapshot returned. |
deduped |
Another same-key request refreshed the origin; this request waited for it. |
bypass |
Request policy skipped caching. |
error |
Reserved for adapter-level error mapping. Core origin errors are rethrown. |
Builds the cache key without reading or writing the store.
Clears entries written by the current response cache namespace. Stored snapshots
always include an internal namespace tag, so Redis and MultiLevel modes use tag
invalidation instead of flushdb. If a future store has no tag invalidation
helper, clear() falls back to the store's own clear().
Deletes one response cache entry by key. The default key is hashed; if your
framework needs human-readable deletion keys, provide a keyBuilder or use a
preset such as createVextResponseCacheOptions().
Invalidates all entries written with the given tag.
Returns cache-hub statistics such as entries, hits, misses, hitRate,
sets, deletes, and memory counters.
Returns the remaining TTL in milliseconds, null for a non-expiring key, or
undefined when the key is missing or TTL lookup is unavailable.
Optional lifecycle hook. Use it during application shutdown when Redis,
MultiLevel, or distributed invalidation is enabled. Memory mode also exposes it,
but existing mocks do not need to implement it because the public contract keeps
close optional.
Creates response cache headers from metadata. By default it emits X-Cache: HIT for hits and X-Cache: MISS for miss-like states. Set
cacheControl: true to also emit Cache-Control: public,max-age=N only when
the result was actually stored in response cache. Responses skipped because of
Set-Cookie, private, no-store, TTL bypass, or cache write failure do not
receive a new public Cache-Control header from this helper.
Use these helpers in framework adapters:
| Helper | Purpose |
|---|---|
normalizeResponseCacheRequest(input) |
Converts framework request fields into ResponseCacheRequest. |
createResponseCacheWritePayload(result, options?) |
Merges cached response headers with X-Cache / Cache-Control helper output. |
createResponseCacheCapture(initial?) |
Captures status, headers, and body for adapters that intercept framework sends. |
createVextResponseCacheOptions(routeCacheConfig, base?) converts vext-style
cache: false | number | { ttl, vary, tags, key, condition } configuration to
ResponseCacheHandleOptions. Number and object ttl values are milliseconds.
condition remains an adapter-side decision: when it
returns false, skip cache.handle() and call the route handler directly.
createVextLegacyKey() produces readable keys such as GET:/products/42 so a
vext adapter can keep app.cache.delete(key) ergonomic.
Returns the underlying cache-hub store for diagnostics or explicit lifecycle
operations.
The core API is framework-agnostic. Framework integrations should adapt the
incoming request to ResponseCacheRequest, call cache.handle(), then write the
returned status, headers, body, and metadata back to the framework response.
import { createServer } from "node:http";
import {
createResponseCache,
createResponseCacheWritePayload,
normalizeResponseCacheRequest,
} from "response-cache-kit";
const cache = createResponseCache({ ttl: 2_000 });
createServer(async (req, res) => {
const result = await cache.handle(
normalizeResponseCacheRequest({
method: req.method,
url: req.url ?? "/",
headers: req.headers,
}),
async () => ({
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ ok: true }),
})
);
const payload = createResponseCacheWritePayload(result, {
cacheControl: true,
});
res.statusCode = payload.status;
for (const [name, value] of Object.entries(payload.headers)) {
res.setHeader(name, value);
}
res.end(String(payload.body));
}).listen(3000);import {
createResponseCache,
createResponseCacheWritePayload,
normalizeResponseCacheRequest,
} from "response-cache-kit";
const cache = createResponseCache({ ttl: 2_000 });
export function responseCacheMiddleware(fetchOrigin) {
return async (req, res, next) => {
try {
const partitionKey = req.user?.id;
const result = await cache.handle(
normalizeResponseCacheRequest({
method: req.method,
url: req.originalUrl ?? req.url,
headers: req.headers,
...(partitionKey ? { partitionKey } : {}),
}),
() => fetchOrigin(req)
);
const payload = createResponseCacheWritePayload(result, {
cacheControl: true,
});
res.status(payload.status);
for (const [name, value] of Object.entries(payload.headers)) {
res.setHeader(name, value);
}
res.send(payload.body);
} catch (error) {
next(error);
}
};
}import {
createResponseCache,
createResponseCacheWritePayload,
normalizeResponseCacheRequest,
} from "response-cache-kit";
const cache = createResponseCache({ ttl: 2_000 });
fastify.get("/products/:id", async (request, reply) => {
const result = await cache.handle(
normalizeResponseCacheRequest({
method: request.method,
url: request.url,
headers: request.headers,
}),
async () => ({
status: 200,
headers: { "content-type": "application/json" },
body: await loadProduct(request.params.id),
})
);
const payload = createResponseCacheWritePayload(result, {
cacheControl: true,
});
reply.code(payload.status);
for (const [name, value] of Object.entries(payload.headers)) {
reply.header(name, value);
}
return payload.body;
});import {
createResponseCache,
createResponseCacheHeaders,
normalizeResponseCacheRequest,
} from "response-cache-kit";
const cache = createResponseCache({ ttl: 2_000 });
app.get("/products/:id", async (c) => {
const result = await cache.handle(
normalizeResponseCacheRequest({
method: c.req.method,
url: c.req.url,
headers: c.req.raw.headers,
}),
async () => ({
status: 200,
headers: { "content-type": "application/json" },
body: await loadProduct(c.req.param("id")),
})
);
for (const [name, value] of Object.entries(createResponseCacheHeaders(result))) {
c.header(name, value);
}
return c.json(result.body, result.status);
});import {
createResponseCache,
createResponseCacheWritePayload,
createVextResponseCacheOptions,
normalizeResponseCacheRequest,
} from "response-cache-kit";
const cache = createResponseCache({ namespace: "vext" });
async function runVextRoute(req, res, route) {
const routeCache = createVextResponseCacheOptions(route.cache);
if (routeCache === false || route.cache?.condition?.(req) === false) {
return route.handler(req, res);
}
const result = await cache.handle(
normalizeResponseCacheRequest({
method: req.method,
url: req.url,
headers: req.headers,
partitionKey: req.user?.id ? `user:${req.user.id}` : undefined,
}),
() => route.handler(req, res),
routeCache
);
const payload = createResponseCacheWritePayload(result, {
cacheControl: true,
});
for (const [name, value] of Object.entries(payload.headers)) {
res.setHeader(name, value);
}
return res.status(payload.status).send(payload.body);
}- Caches
GETandHEADby default. - Uses millisecond TTL values.
- Skips
Set-Cookie,private, andno-storeresponses by default. - Skips
Authorizationrequests unless apartitionKeyis provided. - Filters hop-by-hop headers from cached snapshots.
- Uses same-key single-flight protection for concurrent refreshes.
- Can use Redis lease coordination for cross-process same-key refreshes when
cacheHub.modeis"redis"or"multi-level". - Enables cache-hub tag indexes internally.
If a cached response has ttl: 2_000 and 10000 identical requests arrive after
it expires, only one request refreshes the origin. The other requests wait for
the same in-flight refresh and return the same updated response snapshot.
That default protection is per cache instance. In multi-process deployments, enable Redis lease coordination to reduce cross-process refresh storms:
createResponseCache({
ttl: 2_000,
cacheHub: {
mode: "redis",
url: "redis://localhost:6379",
lease: { waitForOwner: 1_000, onTimeout: "fetch" },
},
});Different keys are independent. Failed origin refreshes are not cached, and a later request can retry.
vext can keep its public cache: false | number | RouteCacheOptions shape by
normalizing route options with createVextResponseCacheOptions().
| vext behavior | response-cache-kit support |
|---|---|
cache: false |
createVextResponseCacheOptions(false) returns false. |
cache: number |
Number is treated as milliseconds. |
ttl object option |
Object ttl is treated as milliseconds. |
vary |
Passed to vary, including explicit vary: "*" when needed. |
tags |
Written to cache-hub tag indexes and invalidated through invalidateTag(). |
| readable deletion key | createVextLegacyKey() returns keys like GET:/products. |
X-Cache / Cache-Control |
createResponseCacheWritePayload(result, { cacheControl: true }). |
| 204 / non-2xx bypass | VEXT_CACHEABLE_STATUSES includes 2xx statuses except 204. |
stale-while-revalidate is intentionally not part of this batch.
stale-while-revalidate: return stale data during a stale window while one background refresh updates the cache.
It needs separate API and failure-semantics decisions before implementation.
npm run typecheck
npm test
npm run test:coverage
npm run build
npm run benchmark
npm run benchmark:http
npm run benchmark:compare
npm run pack:check
npm audit
npm pack --dry-runnpm run benchmark includes an expired-race-10000-same-key scenario. The
expected originCalls value is 1.
Check the request method, request headers, response headers, and TTL:
- Methods other than
GETandHEADbypass unless included incacheableMethods. ttl <= 0bypasses storage.- Requests with
Authorizationbypass unless you passpartitionKeyor enableallowAuthorizationCache. - Responses with
Set-Cookie,Cache-Control: private, orCache-Control: no-storebypass by default.
The request may be producing a different cache key each time. Check url,
query parameters, partitionKey, and vary headers. Avoid putting request IDs,
trace IDs, timestamps, or raw tokens into key inputs.
Prefer partitionKey. For example, use user:${userId} for user-specific data
or tenant:${tenantId}:user:${userId} for multi-tenant user data. Do not use raw
access tokens as partition keys.
Add the headers that change the response to vary:
createResponseCache({
ttl: 30_000,
vary: ["accept-language", "x-tenant-region"],
});The internal cache-hub store stops reading and writing values, so stored cache
hits are disabled. Calls still go through cache.handle(), and same-key
in-flight requests can still be deduplicated during one concurrent wave, but no
stored value is reused after that wave finishes. Use this for local debugging or
temporary cache shutdowns, not as a long-term production setting.
Yes. Use cacheHub.mode: "redis" or cacheHub.mode: "multi-level". The
underlying implementation is still cache-hub; response-cache-kit does not
accept a custom external store. Redis URL mode and distributed invalidation rely
on cache-hub's optional Redis peer, while default Memory mode does not require
Redis.
response-cache-kit only has cache-hub as a runtime dependency. Redis support
is provided by cache-hub's Redis adapter, whose Redis client is optional. Install
the Redis peer in the consuming application when you use URL mode or distributed
invalidation, or pass an existing Redis-like client.
It includes a 10000-request same-key expired-race scenario to verify concurrent
expiry protection. The expected originCalls value is 1.
Apache-2.0