tRPC logo

Skill

caching

configure HTTP caching for tRPC queries

Published by tRPC Updated Jul 18
Covers Performance Backend tRPC Caching

Description

Set 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.

SKILL.md

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

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.