caching
coreSet HTTP cache headers on tRPC query responses via responseMeta callback for CDN and browser caching. Configure Cache-Control, s-maxage, stale-while-revalidate. Handle caching with batching and authenticated requests. Avoid caching mutations, errors, and authenticated responses.
tRPC -- Caching
Setup
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
export const createContext = async (opts: CreateHTTPContextOptions) => {
return {
req: opts.req,
res: opts.res,
user: null as { id: string } | null,
};
};
type Context = Awaited<ReturnType<typeof createContext>>;
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
// server/appRouter.ts
import { publicProcedure, router } from './trpc';
export const appRouter = router({
public: router({
slowQueryCached: publicProcedure.query(async () => {
await new Promise((resolve) => setTimeout(resolve, 5000));
return { lastUpdated: new Date().toJSON() };
}),
}),
});
export type AppRouter = typeof appRouter;
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './trpc';
const server = createHTTPServer({
router: appRouter,
createContext,
responseMeta(opts) {
const { paths, errors, type } = opts;
const allPublic =
paths && paths.every((path) => path.startsWith('public.'));
const allOk = errors.length === 0;
const isQuery = type === 'query';
if (allPublic && allOk && isQuery) {
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
}
return {};
},
});
server.listen(3000);
Core Patterns
Path-based public route caching
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './trpc';
const server = createHTTPServer({
router: appRouter,
createContext,
responseMeta({ paths, errors, type }) {
const allPublic =
paths && paths.every((path) => path.startsWith('public.'));
const allOk = errors.length === 0;
const isQuery = type === 'query';
if (allPublic && allOk && isQuery) {
return {
headers: new Headers([
['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
]),
};
}
return {};
},
});
Name public routes with a public prefix (e.g., public.slowQueryCached) so responseMeta can identify them by path.
Skip caching for authenticated requests
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './trpc';
const server = createHTTPServer({
router: appRouter,
createContext,
responseMeta({ ctx, errors, type }) {
if (ctx?.user || errors.length > 0 || type !== 'query') {
return {};
}
return {
headers: new Headers([
['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
]),
};
},
});
Common Mistakes
[CRITICAL] Caching authenticated responses
Wrong:
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
const server = createHTTPServer({
router: appRouter,
responseMeta() {
return {
headers: new Headers([['cache-control', 's-maxage=60']]),
};
},
});
Correct:
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './trpc';
const server = createHTTPServer({
router: appRouter,
createContext,
responseMeta({ ctx, errors, type }) {
if (ctx?.user || errors.length > 0 || type !== 'query') {
return {};
}
return {
headers: new Headers([
['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
]),
};
},
});
With batching enabled by default, a cached response containing personal data could be served to other users; always check for auth context, errors, and request type before setting cache headers.
Source: www/docs/server/caching.md
[HIGH] Next.js App Router overrides Cache-Control headers
There is no code fix for this -- Next.js App Router overrides Cache-Control headers set by tRPC via responseMeta. The documented caching approach using responseMeta does not work as expected in App Router. Use Next.js native caching mechanisms (revalidate, unstable_cache) instead when deploying on App Router.
Source: https://github.com/trpc/trpc/issues/5625
See Also
- server-setup -- initTRPC, createContext configuration
- adapter-standalone -- responseMeta option on createHTTPServer
- adapter-fetch -- responseMeta option on fetchRequestHandler
- links -- splitLink to separate public/private requests on the client