Cache
Caching lets you store the result of expensive operations — rendered responses, upstream API calls, heavy computations — and serve it again without redoing the work. Nitro offers three ways in:
- Cached handlers — cache the full response of an event handler.
- Cached functions — cache the result of any function and reuse it across handlers.
- Route rules — enable caching for route patterns from your config, without touching handler code.
Cached handlers
To cache an event handler, use the defineCachedHandler method.
It works like defineHandler but with a second parameter for the cache options.
import { defineCachedHandler } from "nitro/cache";
export default defineCachedHandler((event) => {
return "I am cached for an hour";
}, { maxAge: 60 * 60 });
With this example, the response will be cached for 1 hour. Once the cache expires, the next request waits for the handler to resolve a fresh value before responding. If you prefer to immediately serve the stale response while it is revalidated in the background, set swr: true (see SWR behavior).
See the options section for more details about the available options.
varies option to consider specific headers when caching and serving the responses.Automatic HTTP headers
When using defineCachedHandler, Nitro automatically manages HTTP cache headers on cached responses:
etag-- A weak ETag (W/"...") is generated from the response body hash if not already set by the handler.last-modified-- Set to the current time when the response is first cached, if not already set.cache-control-- Automatically set based on theswr,maxAge, andstaleMaxAgeoptions, unless the handler already set one:- With
swr: false(default):max-age=<maxAge> - With
swr: true:s-maxage=<maxAge>, stale-while-revalidate=<staleMaxAge>(a barestale-while-revalidatewhenstaleMaxAgeis unset)
- With
x-cache-- A CDN-style cache status header (HIT,STALE,REVALIDATEDorMISS). Customizable with thecacheStatusHeaderoption.
To keep caching server-side only without advertising cache-control to clients and CDNs, set sendCacheControl: false.
Conditional requests (304 Not Modified)
Cached handlers automatically support conditional requests. When a client sends if-none-match or if-modified-since headers matching the cached response, Nitro returns a 304 Not Modified response without a body.
Request method filtering
Only GET and HEAD requests are cached. All other HTTP methods (POST, PUT, DELETE, etc.) automatically bypass the cache and call the handler directly.
Request deduplication
When multiple concurrent requests hit the same cache key while the cache is being resolved, only one invocation of the handler runs. All concurrent requests wait for and share the same result.
Cached functions
You can also cache a function using defineCachedFunction. This is useful for caching the result of a function that is not an event handler, but is part of one, and reusing it in multiple handlers.
For example, you might want to cache the result of an API call for one hour:
import { defineCachedFunction } from "nitro/cache";
import { defineHandler, type H3Event } from "nitro";
export default defineHandler(async (event) => {
const { repo } = event.context.params;
const stars = await cachedGHStars(repo).catch(() => 0)
return { repo, stars }
});
const cachedGHStars = defineCachedFunction(async (repo: string) => {
const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json());
return data.stargazers_count;
}, {
maxAge: 60 * 60,
name: "ghStars",
getKey: (repo: string) => repo
});
The stars will be cached using the cache storage (in-memory by default) under a key like cache:nitro/functions:ghStars:<owner>/<repo>.json, with value being the number of stars.
{"expires":1677851092249,"value":43991,"mtime":1677847492540,"integrity":"ZUHcsxCWEH"}
In edge workers, the instance is destroyed after each request. Nitro automatically uses event.waitUntil to keep the instance alive while the cache is being updated while the response is sent to the client.
To ensure that your cached functions work as expected in edge workers, you should always pass the event as the first argument to the function using defineCachedFunction.
import { defineCachedFunction } from "nitro/cache";
import { defineHandler, type H3Event } from "nitro";
export default defineHandler(async (event) => {
const { repo } = event.context.params;
const stars = await cachedGHStars(event, repo).catch(() => 0)
return { repo, stars }
});
const cachedGHStars = defineCachedFunction(async (event: H3Event, repo: string) => {
const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json());
return data.stargazers_count;
}, {
maxAge: 60 * 60,
name: "ghStars",
getKey: (event: H3Event, repo: string) => repo
});
This way, the function will be able to keep the instance alive while the cache is being updated without slowing down the response to the client.
Options
The defineCachedHandler and defineCachedFunction functions accept the following options:
Shared options
These options are available for both defineCachedHandler and defineCachedFunction:
Defaults to
/cache (stored under the cache: prefix). When an array is provided, reads try each base in order and writes go to all of them (multi-tier caching).anon_<hash> (computed from the function code) otherwise.'nitro/handlers' for handlers and 'nitro/functions' for functions.String). If not provided, a built-in hash function will be used to generate a key based on the function arguments. For cached handlers, the key is derived from the request URL path and search params.
By default, it is computed from function code, used in development to invalidate the cache when the function code changes.
Defaults to
1 (second).maxAge expires, while revalidation happens in the background. Only applies when swr is enabled. When set to
0, stale values are never served (equivalent to disabling swr). When unset, stale values can be served without a time limit.stale-while-revalidate behavior to serve a stale cached value while asynchronously revalidating it. When enabled, expired cached values are returned immediately while revalidation happens in the background. When disabled, the caller waits for the fresh value once the cache has expired (the stale entry is cleared).
Defaults to
false. See SWR behavior.maxAge / staleMaxAge options for that entry. Return a number of seconds (shorthand for maxAge) or an object to also override staleMaxAge. Useful for values that carry their own expiry, such as access tokens:
getMaxAge: (entry) => entry.value.expires_in. A resolved value <= 0 disables caching for that entry.boolean to invalidate the current cache and create a new one.boolean to bypass the current cache without invalidating the existing entry.By default, errors are logged to the console and captured by the Nitro error handler.
Handler-only options
These options are only available for defineCachedHandler:
true, skip full response caching and only handle conditional request headers (if-none-match, if-modified-since) for 304 Not Modified responses. The handler is called on every request but benefits from conditional caching.Vary header. Headers not listed in
varies are stripped from the request before calling the handler to ensure consistent cache hits. For multi-tenant environments, you may want to pass
['host', 'x-forwarded-host'] to ensure these headers are not discarded and that the cache is unique per tenant.cookie header the handler sees. By default, no cookies are allowed: the
cookie request header is stripped before the handler runs, and any set-cookie header is dropped from the response before it is cached or returned, so a per-user cookie (such as a session id) can never leak to another user through the cache. Only allowlist cookies whose values are safe to share across every user hitting the same cache key (e.g. a theme or locale preference) — never per-user secrets.cache-control response header. Defaults to true. Set to
false for server-only caching: responses are still stored and served from cache, but no cache-control header is advertised to clients and CDNs.X-Cache: HIT | STALE | REVALIDATED | MISS). Defaults to true. Pass a string to use a custom header name, or false to disable.false to skip caching a response — it is still returned to the caller, just not stored.Function-only options
These options are only available for defineCachedFunction:
The entry also carries a per-call
entry.status ("hit", "stale", "revalidated" or "miss") describing how the value was served — useful for metrics or conditional logic.transform). Useful when the function returns something that cannot be stored as-is (e.g. a stream or a class instance): serialize converts it on write and transform restores it on read.false (or a Promise resolving to false) to treat the entry as invalid and trigger re-resolution. The second argument carries the args of the current call, so the entry can be validated against it.SWR behavior
The stale-while-revalidate (SWR) pattern is opt-in via the swr option (disabled by default). Understanding how it interacts with other options:
swr | maxAge | Behavior |
|---|---|---|
false (default) | 3600 | Cache for 1 hour, wait for the fresh value when expired |
true | 3600 | Cache for 1 hour, serve stale while revalidating |
true | 3600 with staleMaxAge: 600 | Cache for 1 hour, serve stale for up to 10 more minutes while revalidating |
true | 3600 with staleMaxAge: 0 | Cache for 1 hour, never serve stale (same as swr: false) |
When swr is enabled and a cached value exists but has expired:
The stale cached value is returned immediately to the client.
The function/handler is called in the background to refresh the cache.
On edge workers, event.waitUntil is used to keep the background refresh alive.
When swr is disabled (default) and a cached value has expired:
The stale entry is cleared.
The client waits for the function/handler to resolve with a fresh value.
swr when slightly outdated data is acceptable — content pages, listings, mirrored third-party APIs — so response times stay flat even when the cache expires. Keep it disabled when callers must never receive stale data, such as access tokens or per-request authorization checks.Cache keys
When using the defineCachedFunction or defineCachedHandler functions, the cache key is generated using the following pattern:
`${options.base}:${options.group}:${options.name}:${options.getKey(...args)}.json`
For example, the following function:
import { defineCachedFunction } from "nitro/cache";
const getAccessToken = defineCachedFunction(() => {
return String(Date.now())
}, {
maxAge: 10,
name: "getAccessToken",
getKey: () => "default"
});
Will generate the following cache key:
cache:nitro/functions:getAccessToken:default.json
The default base is /cache, which the storage layer normalizes to the cache: prefix.
varies option, hashes of the specified header values appended to the key.>= 400, an undefined body, a cache-control header containing no-store or private, or missing etag / last-modified headers. This prevents caching error responses and responses explicitly marked as non-cacheable. Use the shouldCache option to further narrow what gets cached.Cache invalidation
Cached entries can be invalidated programmatically at runtime (for example from a webhook when the underlying data changes) without waiting for maxAge to expire.
.invalidate() and .expire() methods
Every function created with defineCachedFunction (and every handler created with defineCachedHandler) exposes on-demand revalidation methods:
.invalidate(...args)— removes the cached entry entirely. The next call re-invokes the function and waits for the fresh value..expire(...args)— marks the cached entry as stale without removing it. Withswrenabled, the stale value is still served while the next call triggers a background refresh; without SWR, the next call re-resolves before returning..resolveKeys(...args)— resolves the storage key(s) the entry is cached under (one perbaseprefix).
Arguments are passed through getKey to generate the cache key. For cached handlers, pass the event (e.g. .invalidate(event)).
import { defineCachedFunction } from "nitro/cache";
const cachedGHStars = defineCachedFunction(async (repo: string) => {
const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json());
return data.stargazers_count;
}, {
maxAge: 60 * 60,
name: "ghStars",
getKey: (repo: string) => repo,
});
await cachedGHStars("unjs/nitro"); // populates the cache
await cachedGHStars.expire("unjs/nitro"); // marks the entry as stale
await cachedGHStars.invalidate("unjs/nitro"); // removes the entry
await cachedGHStars("unjs/nitro"); // re-invokes the function
If no cached entry matches the given arguments, .invalidate() and .expire() resolve without error and leave storage unchanged.
Standalone helpers
The invalidateCache(), expireCache(), and resolveCacheKeys() helpers work from the cache options used to define a cached function, so invalidation can live anywhere in your app, independent of where the cached function is defined.
Pass the same name, group, base, and getKey used when defining the function, along with the args identifying the entry:
import { invalidateCache } from "ocache";
await invalidateCache({
options: {
name: "ghStars",
group: "nitro/functions",
getKey: (repo: string) => repo,
},
args: ["unjs/nitro"],
});
expireCache() accepts the same input and marks the entry as stale instead of removing it, while resolveCacheKeys() returns the storage keys so you can operate on them directly.
nitro/cache — import them from ocache directly. Since ocache is a transitive dependency of Nitro, add it to your own package.json dependencies to depend on it explicitly.name, group, base, and getKey passed to these helpers must match the ones used when the cached function was defined. Mismatched options resolve to a different storage key and will not affect the intended entry. Nitro defaults to
group: 'nitro/functions' for cached functions and group: 'nitro/handlers' for cached handlers (or 'nitro/route-rules' when using route rules).Cache storage
Cache entries are stored using the storage layer under the cache: prefix (derived from the default base: "/cache" option).
By default, no dedicated mount point is configured for it: entries live in the root storage, which uses an in-memory driver and is not persisted across restarts — in both development and production.
To use a persistent backend, set the cache mount point using the storage option:
import { defineConfig } from "nitro";
export default defineConfig({
storage: {
cache: {
driver: 'redis',
/* redis connector options */
}
}
})
In development, you can also overwrite the cache mount point using the devStorage option:
import { defineConfig } from "nitro";
export default defineConfig({
storage: {
cache: {
// production cache storage
},
},
devStorage: {
cache: {
// development cache storage
}
}
})
Using route rules
Route rules let you enable caching for all routes matching a glob pattern, directly from your configuration. This is especially useful for a global cache strategy across a part of your application, without touching handler code.
Cache all the blog routes for 1 hour:
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
"/blog/**": { cache: { maxAge: 60 * 60 } },
},
});
If we want to use a custom cache storage mount point, we can use the base option.
import { defineConfig } from "nitro";
export default defineConfig({
storage: {
redis: {
driver: "redis",
url: "redis://localhost:6379",
},
},
routeRules: {
"/blog/**": { cache: { maxAge: 60 * 60, base: "redis" } },
},
});
Route rules shortcuts
You can use the swr shortcut for enabling stale-while-revalidate caching on route rules. When set to true, SWR is enabled with the default maxAge. When set to a number, it is used as the maxAge value in seconds.
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
"/blog/**": { swr: true },
"/api/**": { swr: 3600 },
},
});
To explicitly disable caching on a route, set cache: false:
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
"/api/realtime/**": { cache: false },
},
});
'nitro/route-rules' instead of the default 'nitro/handlers'.