"The API is slow" and "the page is slow to render" have the same first question, where does the time go, and the same first mistake, adding a cache before knowing. This page is the Nitro toolkit for caching correctly and the list of server costs that only code changes fix. The performance section covers measurement in depth; the HTTP page covers the headers.
Vary; s-maxage, swr/isr route rules) → Nitro response cache (defineCachedEventHandler, routeRules.cache) → function cache (cachedFunction) → database and upstream caches (Redis, query cache) → the query itself. Cache as far out as correctness allows; personalised data never in a shared layer.defineCachedEventHandler(handler, options) caches the whole response: maxAge (seconds), swr (serve stale while revalidating, default true), staleMaxAge, getKey(event) (default: the URL; include what changes the result, never the whole cookie), varies (headers that join the key), name/group (storage prefixes for targeted clearing), shouldBypassCache, shouldInvalidateCache, base (storage mount), headersOnly. routeRules.cache does the same declaratively ({ maxAge, swr, varies }) and swr: N/isr: N are shortcuts.cachedFunction(fn, options) (alias defineCachedFunction) memoises any async function with the same options plus getKey(...args): right for expensive upstream calls or computations shared by several routes.cache storage mount, in memory by default, which does not survive restarts and is not shared between instances or serverless invocations; production sets nitro.storage.cache to Redis, Cloudflare KV, or the platform's store. Cache size and TTLs must be bounded (memory driver + unbounded keys = a leak).name/group prefixes and clear with useStorage('cache').clear('nitro:handlers:name') or removeItem(key) after mutations, keep TTLs short enough that stale data is acceptable, or use shouldInvalidateCache for explicit bypasses (an admin ?refresh=1 with auth). CDN caches are purged through the provider API or by changing URLs.dedupe: 'defer' on useAsyncData.Set-Cookie; include locale, currency and device class in the key when they change the output; return Vary to the CDN for the same headers.Promise.all, lazy: true for non-critical data), N+1 queries (data layer), oversized payloads (pick/transform, DTOs), heavy synchronous work in handlers (move to tasks or workers), large dependency graphs (nuxt analyze for the server bundle; defineLazyEventHandler), and cold starts on serverless and edge (smaller server bundles, fewer top-level imports, lazy handlers, provisioned concurrency where available).experimental.ssrStreaming (4.5, off by default) flushes the shell early; handlers can return ReadableStreams so large exports never buffer.AbortSignal.timeout), retries with backoff only for idempotent calls, circuit breakers for flaky upstreams, rate limits, bounded pagination and body sizes (security).Server-Timing headers per phase, structured logs with cache hit/miss, request ids, p50/p95/p99 latencies per route, and a load test (autocannon, k6) against the built server before and after each change.Response caching with a correct key and targeted invalidation:
export default defineCachedEventHandler(async (event) => {
const { category = 'all', locale = 'en' } = getQuery(event) as Record<string, string>
const products = await listPublishedProducts(useDb(), { category })
return products.map(p => toProductDto(p, locale))
}, {
name: 'catalog', // storage prefix: nitro:handlers:catalog:*
maxAge: 60 * 5,
swr: true, // serve stale, revalidate in the background
staleMaxAge: 60 * 60,
getKey: (event) => {
const { category = 'all', locale = 'en' } = getQuery(event) as Record<string, string>
return `${locale}:${category}` // everything that changes the output, nothing that identifies the user
},
shouldBypassCache: event => getHeader(event, 'x-preview') === useRuntimeConfig(event).previewToken,
})
export default defineEventHandler(async (event) => {
requireRole(event, 'admin')
const product = await updateProduct(useDb(), getRouterParam(event, 'id')!, await readValidatedBody(event, updateProductSchema.parse))
await useStorage('cache').clear('nitro:handlers:catalog') // targeted invalidation by prefix; the CDN follows on its own swr
return toProductDto(product)
})
A cached upstream call shared by routes, with a Redis-backed store in production:
export const getExchangeRates = cachedFunction(async (base: string) => {
return $fetch<Record<string, number>>('https://rates.example.com/latest', { query: { base }, signal: AbortSignal.timeout(2_000) })
}, {
name: 'rates',
maxAge: 60 * 15,
swr: true,
getKey: base => base.toUpperCase(),
})
export default defineNuxtConfig({
routeRules: {
'/products/**': { swr: 300 }, // rendered HTML, cached by Nitro, served stale-while-revalidate
'/api/catalog/**': { cache: { maxAge: 300, swr: true, varies: ['accept-language'] } },
'/api/account/**': { headers: { 'cache-control': 'private, no-store' } },
},
nitro: {
storage: { cache: { driver: 'redis', url: process.env.REDIS_URL } }, // shared across instances; memory in dev
experimental: { openAPI: true },
},
})
Finding the real cost before caching: timings per phase and a load test.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', (event) => { event.context.t0 = performance.now() })
nitroApp.hooks.hook('beforeResponse', (event) => {
const total = performance.now() - event.context.t0
setResponseHeader(event, 'server-timing', `total;dur=${total.toFixed(1)}${event.context.dbMs ? `, db;dur=${event.context.dbMs.toFixed(1)}` : ''}`)
if (total > 500) console.warn(JSON.stringify({ level: 'warn', msg: 'slow request', path: event.path, total, requestId: event.context.requestId }))
})
})
pnpm nuxt build && node .output/server/index.mjs &
npx autocannon -c 50 -d 20 http://localhost:3000/api/catalog/products # p99 before
# add the cached handler, rebuild, repeat; compare p99 and the database query count
defineCachedEventHandler keyed by URL on a route that reads event.context.user serves the first user's data to everyone for maxAge. Either the key includes the user (and the cache is per user, which rarely pays off) or the personalised part moves out of the cached response.
With the default memory driver, an invalidation on instance A leaves instance B serving stale data until its TTL, and a serverless function's cache disappears with the instance. Shared storage (Redis, KV) is the production configuration, and TTLs must assume invalidation can miss.
The current defineCachedEventHandler option names (swr, staleMaxAge, varies, shouldBypassCache) and the storage key prefix used for handlers, plus whether experimental.ssrStreaming is still experimental in the latest 4.x.
autocannon before and after defineCachedEventHandler; record p99 and the number of queries."First I measure where the time goes with Server-Timing and a load test, because a slow query or a sequential chain of awaits is fixed in code, not hidden by a cache. When the response is shared and the staleness is acceptable, I wrap the handler in defineCachedEventHandler with a key that contains exactly what changes the output, locale, category, page, and nothing that identifies the user, a short maxAge with swr so users never wait on revalidation, and a name prefix so a mutation can clear the entries for that resource. Storage is Redis or the platform KV in production because the default memory cache is per instance. At the edge I add swr route rules or s-maxage headers so the CDN takes most of the traffic, and personalised routes are marked private, no-store and stay out of every shared layer."
Nothing yet. Notes filed here show up on this page and the section overview.
Auth and sessions in Nuxt
Implementing authentication end to end with nuxt-auth-utils — sealed sessions, password and OAuth login, passkeys, session hooks — plus authorisation as data with role and object checks, SSR cookie forwarding, multi-tenant context, logout and invalidation, and the alternatives (Better Auth, Auth.js, hosted providers).
Files, realtime and background work
Uploads through the server or with presigned URLs, validation and processing, serving files safely; WebSockets and server-sent events from the server's point of view and how they scale; Nitro tasks and scheduled tasks, queues, idempotency, retries and the outbox pattern; and post-response work with waitUntil.