LearnFrenzy
Sign in Take a free test

Caching and Revalidation · Lesson 12 of 24

Caching in Next.js 16 with "use cache"

6 min read Updated Nov 8, 2026 Saurabh Samir

Fetching fresh data on every request is simple but wasteful: the course catalogue doesn't change between two visitors a second apart. Caching stores a result so later requests reuse it. In Next.js 16 with Cache Components, caching is explicit: nothing is cached unless you mark it with the "use cache" directive, and you choose how long it lives with cacheLife.

In this lesson

  • "use cache" on functions and components
  • Static, cached and dynamic content on one page – measured
  • cacheLife profiles and cache keys
  • What can't go inside a cache

Three kinds of content

KindExampleHow you write it
Staticheadings, marketing textnothing special – prerendered automatically
Cachedcourse catalogue, blog posts, prices"use cache" + cacheLife
Dynamicthe user's cart, live stock, anything using cookiesinside <Suspense>, runs per request

"use cache" in action

This page has a cached catalogue and a per-request view counter. Each function counts how often its body really runs:

// lib/catalog.ts
import { cacheLife } from "next/cache";

let runs = 0;

export async function getCatalog() {
  "use cache";                                   // cache this function's result
  cacheLife("hours");                            // fresh for an hour, then revalidated
  runs++;
  console.log(`getCatalog() body ran (run #${runs})`);
  await new Promise((r) => setTimeout(r, 300));  // an expensive query
  return ["React.js", "Next.js", "TypeScript"];
}
// app/page.tsx
import { Suspense } from "react";
import { connection } from "next/server";
import { getCatalog } from "@/lib/catalog";

let views = 0;

async function ViewCounter() {
  await connection();                            // dynamic: runs on every request
  views++;
  return <p>You are visitor #{views} since the server started</p>;
}

export default async function Home() {
  const courses = await getCatalog();            // cached: part of the static shell
  return (
    <main>
      <h1>Course catalogue</h1>
      <ul>{courses.map((c) => <li key={c}>{c}</li>)}</ul>
      <Suspense fallback={<p>Counting…</p>}>
        <ViewCounter />
      </Suspense>
    </main>
  );
}

Build output, three visits and the server log

Route (app)      Revalidate  Expire
┌ ◐ /                    1h      1d


○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

Course catalogue
• React.js
• Next.js
• TypeScript
You are visitor #1 since the server started

› reload the page
Course catalogue
• React.js
• Next.js
• TypeScript
You are visitor #2 since the server started

› reload the page
Course catalogue
• React.js
• Next.js
• TypeScript
You are visitor #3 since the server started

What happened:

  • At build time, getCatalog() ran and its result was baked into the static shell – the server log never shows it at runtime, and all three visits get the list instantly.
  • The visitor counter is inside Suspense with connection(), so it's a dynamic hole that runs for every request: #1, #2, #3.
  • The route is ◐ Partial Prerender: static and cached parts are sent at once; dynamic parts stream in. The build table also shows when the cached shell will be revalidated (1h) and expire (1d), taken from cacheLife("hours").

Caching a component

"use cache" also works at the top of an async component (or a whole page or layout) to cache its rendered output:

import { cacheLife, cacheTag } from "next/cache";

async function PopularCourses() {
  "use cache";
  cacheLife("days");
  cacheTag("courses");                         // a label for on-demand revalidation (next lesson)
  const courses = await db.course.findMany({ orderBy: { enrolments: "desc" }, take: 5 });
  return <ul>{courses.map((c) => <li key={c.id}>{c.title}</li>)}</ul>;
}

Putting "use cache" at the very top of a file caches every exported async function in it.

cacheLife profiles

Profilestale (browser)revalidate (server)expire
default5 min15 minnever
seconds30 s1 s1 min
minutes5 min1 min1 hour
hours5 min1 hour1 day
days5 min1 day1 week
weeks5 min1 week30 days
max5 min30 days1 year
  • stale – how long the browser may reuse the result without asking the server.
  • revalidate – after this, the next request still gets the cached result instantly while a fresh one is generated in the background (stale-while-revalidate).
  • expire – after this, requests wait for fresh data.

You can also pass your own values: cacheLife({ stale: 60, revalidate: 300, expire: 3600 }) (seconds). Very short lifetimes – the seconds profile or an expiry under five minutes – aren't worth prerendering, so Next.js treats them as dynamic holes instead.

Cache keys

Arguments are part of the cache key, so each distinct input gets its own entry:

// lib/courses.ts
import { cacheLife } from "next/cache";

const data: Record<string, number> = { react: 31, nextjs: 24 };
let runs = 0;

export async function getLessonCount(slug: string) {
  "use cache";
  cacheLife("hours");
  runs++;
  console.log(`getLessonCount("${slug}") ran – total runs: ${runs}`);
  return data[slug] ?? 0;
}
// app/page.tsx
import { Suspense } from "react";
import { connection } from "next/server";
import { getLessonCount } from "@/lib/courses";

async function Counts() {
  await connection();                              // look the counts up at request time…
  const react = await getLessonCount("react");     // …through the cache
  const next = await getLessonCount("nextjs");
  const reactAgain = await getLessonCount("react");
  return <p>React {react}, Next.js {next}, React again {reactAgain}</p>;
}

export default function Home() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Counts />
    </Suspense>
  );
}

Two visits and the server log

React 31, Next.js 24, React again 31
server: getLessonCount("react") ran – total runs: 1
server: getLessonCount("nextjs") ran – total runs: 2

› reload the page
React 31, Next.js 24, React again 31

Two different arguments, two runs – and on the second visit, none at all: both results came from the server's in-memory cache. (On serverless platforms each instance has its own memory, so runtime cache hits are less predictable; "use cache: remote" plugs in a shared store such as Redis.)

What can't go inside "use cache"

  • cookies(), headers() and searchParams can't be read inside a cached scope – a shared cache entry must not depend on one user's request. Read them outside and pass the value in as an argument (it then becomes part of the key).
  • Arguments and return values must be serializable.
  • Never cache per-user secrets in a shared cache by accident: if the result depends on the user, the user's ID must be an argument.

Practice

Try it yourself

  1. Cache a getPrice(sku) function for minutes, and show prices for two products on a static page.

    Show solution
    // lib/prices.ts
    import { cacheLife } from "next/cache";
    
    const prices: Record<string, number> = { "KB-01": 3499, "MS-02": 899 };
    
    export async function getPrice(sku: string) {
      "use cache";
      cacheLife("minutes");
      return prices[sku];
    }
    // app/page.tsx
    import { getPrice } from "@/lib/prices";
    
    export default async function Shop() {
      const [keyboard, mouse] = await Promise.all([getPrice("KB-01"), getPrice("MS-02")]);
      return (
        <ul>
          <li>Keyboard ₹{keyboard}</li>
          <li>Mouse ₹{mouse}</li>
        </ul>
      );
    }

    Build output and page

    Route (app)      Revalidate  Expire
    ┌ ○ /                    1m      1h
    
    
    ○  (Static)  prerendered as static content
    
    • Keyboard ₹3499
    • Mouse ₹899

Quick check

Is data cached by default in Next.js 16 with Cache Components?

No – only what you mark with "use cache".

What becomes part of a cached function's key?

Its arguments (and values captured from outer scopes).

Can a cached function call cookies()?

No – read cookies outside and pass the value in.

Summary

  • "use cache" caches a function's or component's result; cacheLife sets how long it stays fresh.
  • Static and cached content forms the prerendered shell; dynamic parts stream inside Suspense.
  • Arguments form the cache key; request APIs can't be read inside a cache.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links