Next.js App Router in Production · Rendering, Caching and Mutations

Caching with use cache, cacheLife and cacheTag

Cache data and UI with Cache Components: use cache with cacheLife and cacheTag, stream per-request content in Suspense, and revalidate on demand.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A home page that mixes three kinds of content: a static header, a cached product list (tagged products), and a per-request greeting read from a cookie and streamed in Suspense. A Route Handler adds a product and calls revalidateTag("products", "max"), and the logs show when the database is actually read.

The idea

  • "use cache" caches the result of an async function or component; arguments become part of the cache key.
  • cacheLife sets how long entries are fresh (profiles like "hours", "days", or custom stale/revalidate/expire); cacheTag labels entries for on-demand invalidation.
  • Runtime data (cookies(), headers(), searchParams) can't be in the static shell; read it inside <Suspense> so it streams per request.
  • revalidateTag(tag, "max") marks entries stale: the next request gets the stale content immediately while fresh content is generated in the background. updateTag(tag) (Server Actions only) expires immediately for read-your-own-writes.

The code

Static, cached and per-request content on one page
// file: next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = { cacheComponents: true, partialPrefetching: true };
export default nextConfig;

// file: app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body><main>{children}</main></body>
    </html>
  );
}

// file: app/lib/db.ts
import "server-only";

const store = globalThis as unknown as { products?: string[] };
store.products ??= ["Mug", "Teapot"];
export const db = { products: store.products };

// file: app/lib/catalog.ts
import { cacheLife, cacheTag } from "next/cache";
import { db } from "./db";

export async function getProducts() {
  "use cache";
  cacheLife("hours");
  cacheTag("products");
  console.log("[app] reading products from the database");
  return [...db.products];
}

// file: app/page.tsx
import { Suspense } from "react";
import { cookies } from "next/headers";
import { getProducts } from "./lib/catalog";

async function Products() {
  const products = await getProducts();
  return <ul>{products.map((name) => <li key={name}>{name}</li>)}</ul>;
}

async function Greeting() {
  const name = (await cookies()).get("name")?.value ?? "guest";
  return <p>Hello, {name}</p>;
}

export default function Page() {
  return (
    <>
      <h1>Kitchen shop</h1>
      <Suspense fallback={<p>Hello...</p>}>
        <Greeting />
      </Suspense>
      <Products />
    </>
  );
}

// file: app/api/products/route.ts
import { revalidateTag } from "next/cache";
import { db } from "../../lib/db";

export async function POST(request: Request) {
  const { name } = await request.json();
  db.products.push(name);
  revalidateTag("products", "max");
  return Response.json({ added: name }, { status: 201 });
}

// request: GET / cookie:name=Ana
// request: GET / cookie:name=Ben
// request: POST /api/products {"name": "Kettle"}
// request: GET /
// wait: 1
// request: GET /

Output

GET / -> 200
Kitchen shop
Hello, Ana
Mug
Teapot
GET / -> 200
Kitchen shop
Hello, Ben
Mug
Teapot
POST /api/products -> 201
{"added": "Kettle"}
GET / -> 200
[app] reading products from the database
Kitchen shop
Hello, Ben
Mug
Teapot
GET / -> 200
Kitchen shop
Hello, Ben
Mug
Teapot
Kettle

Run this on your computer — see “Get Started” for setup.

The product list was cached at build time, so the first two requests don't touch the database while each still gets its own greeting. After revalidateTag(..., "max"), the next request is served the stale list immediately and triggers a background refresh (the log line); the request after that sees "Kettle".

Common mistakes

  • Reading cookies or headers inside a "use cache" function; pass the values in as arguments instead (they become part of the key) or use "use cache: private".
  • Caching without a tag, then having no precise way to invalidate after a mutation.
  • Using revalidateTag where the user must see their own change at once; use updateTag in the Server Action.
  • Running several instances with the default in-memory cache and expecting shared invalidation; configure a shared cache handler.

Key points

  • use cache + cacheLife + cacheTag for data and UI that can be shared.
  • Runtime data streams inside Suspense.
  • revalidateTag(tag, "max") for stale-while-revalidate, updateTag for read-your-own-writes.

Further reading: Caching · Revalidating · use cache

Practice questions

What becomes part of a use cache entry's key?

Answer

The function's arguments and any values it captures from its surrounding scope, plus the build; different inputs create separate entries.

What's the difference between revalidateTag(tag, "max") and updateTag(tag)?

Answer

revalidateTag marks entries stale and serves stale content while refreshing in the background; updateTag (Server Actions only) expires them immediately so the user sees their change.

Why wrap cookie-reading components in Suspense?

Answer

Runtime data is only available per request; inside Suspense the rest of the page can be prerendered into the static shell while that part streams.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?