Cache & Deduplication
Neutrx ships an in-memory response cache and in-flight request deduplication. Both are on by default for safe methods and are configured under performance.
Response cache
const api = neutrx.create({
performance: {
enableCaching: true,
cacheStrategy: 'swr',
cacheTTL: 300_000, // default max-age when upstream sends none (ms)
revalidateAfter: 60_000, // optional SWR freshness boundary
cacheStaleMax: 1_500_000, // max window stale SWR entries stay usable
cacheMaxSize: 500, // max entries
cacheMaxEntrySize: 1_048_576, // 1 MB per entry
respectCacheHeaders: true,
onRevalidate: e => console.log(e.url, e.updated, e.status),
},
});
Options
| Option | Type | Default | Meaning |
|---|---|---|---|
enableCaching |
boolean |
true |
Master toggle |
cacheStrategy |
'max-age' \| 'swr' \| 'network-first' |
'max-age' |
Freshness policy (see below) |
cacheTTL |
number (ms) |
300000 |
Default max-age when upstream omits one |
revalidateAfter |
number (ms) |
— | SWR freshness cap before background refresh |
cacheStaleMax |
number (ms) |
max(cacheTTL, 1500000) |
Bounded window stale entries remain usable |
cacheMaxSize |
number |
500 |
Max cached entries (LRU eviction) |
cacheMaxEntrySize |
number (bytes) |
1048576 |
Skip caching responses larger than this |
respectCacheHeaders |
boolean |
true |
Honor Cache-Control + Expires |
cacheAdapter |
CacheStore |
in-memory | Custom process-local store |
onRevalidate |
(event) => void |
— | Fires after a background revalidation |
ttlandstale-while-revalidateare accepted as compatibility aliases formax-ageandswr.
What gets cached
- Only successful 2xx responses, for cacheable methods (
GETby default). - The cache key is a SHA-256 of
{ socketPath, url, Accept, Authorization }— so per-user/per-tenant responses don’t collide. Cache-Control: no-store,no-cache, andprivateskip caching entirely.max-age/Expires(whenrespectCacheHeaders) overridecacheTTL.
On a hit, the response carries cached: true, plus cacheAge and (for stale serves) stale: true with an x-cache: STALE header.
Strategies
| Strategy | Behavior |
|---|---|
max-age (default) |
Serve fresh hits until expiry; then go to network. |
swr |
Serve fresh hits immediately. After revalidateAfter / max-age, return stale data immediately (cached: true, stale: true) while one background request revalidates. Conditional If-None-Match / If-Modified-Since are sent automatically. |
network-first |
Try the network first; on network failure fall back to a cached entry within its stale window. |
stale-if-error is always available: if the network fails and a cached entry is within its stale-if-error window (from the Cache-Control directive), Neutrx serves it with x-cache: STALE-IF-ERROR and a Warning: 110 header rather than throwing.
Manage the cache
await api.get('/catalog');
api.getCacheStats(); // { hits, misses, evictions, size, hitRate, ... }
api.clearCache(); // clear all
api.invalidateCache(/\/catalog/u); // by pattern
api.deleteCacheEntry('/catalog'); // single entry
Custom cache store
const api = neutrx.create({
performance: {
cacheAdapter: {
get: key => store.get(key),
set: (key, value) => store.set(key, value),
delete: key => store.delete(key),
clear: () => store.clear(),
keys: () => store.keys(),
lock: key => lockOnce(key), // optional: single-flight revalidation
unlock: key => unlock(key), // optional
},
},
});
Core stays synchronous and dependency-free. Redis or other networked stores belong in optional packages that own async locking, serialization, and peer dependencies.
Request deduplication
Identical in-flight requests are coalesced into one network call; the joiners get a clone with deduplicated: true.
| Option | Type | Default |
|---|---|---|
deduplicateRequests |
boolean |
true |
deduplicateMethods |
HttpMethod[] |
['GET', 'HEAD'] |
deduplicateHeaders |
string[] |
['accept', 'authorization', 'range'] |
deduplicateRequestKey |
(config) => string \| null \| undefined |
— (default key) |
const api = neutrx.create({
performance: {
deduplicateRequestKey: c =>
`${c.method}:${c.url}:${c.headers.get('X-Tenant-ID') ?? ''}`,
},
});
The default key includes method, final URL with serialized params, response type, adapter, socket path, the keyable transport limits (timeout, redirect/response limits, proxy-disabled, HTTP/2 settings), and the selected headers.
Requests with a cancellation
signal/cancelToken,responseType: 'stream', or non-keyable transport overrides (customproxy,tls, agents,lookup,fetch, redirect hook,maxRate) are never deduplicated by the default key — so one caller’s cancellation or transport can’t change another’s. ReturnnullfromdeduplicateRequestKeyto skip dedup for a request.
Coalescing methods beyond GET/HEAD is opt-in; only do it when the requests are genuinely equivalent (e.g. include an idempotency key in your custom key).
Joined requests count in api.getMetrics().requests.deduplicated and the neutrx_deduplication_hits_total Prometheus counter, and emit a deduplication:hit event. See Observability.
Related
- Pagination · Request Batching (DataLoader)
- Config Reference — full performance schema
Frequently asked questions
How does caching work in Neutrx?
Neutrx caches responses according to Cache-Control semantics and supports stale-while-revalidate (serve stale, refresh in background), stale-if-error (serve stale when the origin fails), and network-first modes, backed by a default or custom store.
What is request deduplication in Neutrx?
In-flight deduplication collapses identical concurrent requests into a single network call, so N callers asking for the same resource at once share one response instead of issuing N requests.
Can I use a custom cache store?
Yes. Provide a store implementing the cache interface (for example Redis or an LRU) to share cached responses across workers or processes instead of the default in-memory store.
What is stale-if-error?
stale-if-error serves a previously cached response when a fresh request to the origin fails, trading absolute freshness for availability during upstream outages.