Next.js 16 Cache Components in Practice: use cache, cacheTag and updateTag
How we rebuilt this site's CMS-driven pages on Next.js 16 Cache Components — instant static pages, content that updates the moment you hit save, and a database fallback that never breaks the site.

Ahsan Bilal
Principal Software Engineer
On this page
Next.js 16 makes caching explicit. Instead of guessing which fetch calls are cached and which routes are dynamic, you turn on Cache Components and mark exactly what to cache with the "use cache" directive. We migrated this site — a portfolio with a MongoDB-backed admin CMS — to that model. Here's what we learned.
The mental model
With cacheComponents: true in next.config.ts, everything is dynamic by default at request time, and you opt into caching per function or component:
// next.config.ts
const nextConfig: NextConfig = {
cacheComponents: true,
};At build time, Next.js prerenders as much of each route as it can into a static shell. Anything that depends on the request — cookies, headers, uncached data — must sit inside a <Suspense> boundary and streams in afterwards. The result is Partial Prerendering by default: instant HTML for the static parts, streamed HTML for the rest.
Caching data with "use cache"
Our content layer is a set of small functions, each cached and tagged:
import { cacheLife, cacheTag } from "next/cache";
export async function getPublishedPosts() {
"use cache";
cacheTag("posts");
cacheLife("max");
return db.post.findMany({
where: { status: "PUBLISHED" },
orderBy: { publishedAt: "desc" },
});
}Three things are happening:
"use cache"caches the function's return value, keyed by its arguments.cacheTag("posts")labels the entry so we can invalidate it later.cacheLife("max")says "keep this as long as possible" — we'll invalidate explicitly instead of relying on timers.
Pages simply call these functions. Because the data is cached, the blog index and every post can be fully prerendered at build time.
Invalidating on save with updateTag
When an admin saves a post, the server action writes to the database and expires the relevant tags:
"use server";
import { updateTag } from "next/cache";
export async function savePost(id: string, formData: FormData) {
await requireAdmin();
const post = await db.post.update({ where: { id }, data: parse(formData) });
updateTag("posts");
updateTag(`post:${post.slug}`);
}updateTag is designed for exactly this read-your-own-writes case: it expires the cache immediately, so the editor who just hit save sees the new content on their next request. (For background revalidation where slightly stale content is fine, revalidateTag with a cache-life profile is the alternative.) Note that updateTag is only available in Server Actions.
With tags in place, one save refreshes the blog index, the post page, the home page's "latest articles", the RSS feed and the sitemap — because all of them read through the same tagged functions.
Choosing cache lifetimes
cacheLife profiles control how long an entry is considered fresh and when it expires. We use three:
| Data | Profile | Why |
|---|---|---|
| CMS content (posts, projects, profile) | max | Changes only when an admin saves; tags handle invalidation |
| Home page (includes GitHub stats) | hours | External data we don't control |
| Footer | days | Rarely changes |
Keeping request-specific code out of the cache
You can't read cookies or headers inside a "use cache" scope — the cached result would leak between users. Our admin panel reads the session cookie, so admin pages keep the data fetching dynamic and wrap it in Suspense. The layout and loading skeleton still prerender, so the admin shell appears instantly while the authenticated content streams in.
A related gotcha: values like Date.now() or Math.random() during prerendering make output non-deterministic. Next.js 16 flags these at build time. Our inquiry form recorded a timestamp in component state for spam detection; the build failed until we moved the read into an effect that runs only in the browser.
A fallback that never breaks the site
The cached functions wrap database calls with a fallback: if DATABASE_URL is missing or the database is unreachable, they return content from Markdown files in the repository. Builds succeed without a database, local development works offline, and a database outage degrades to slightly older content rather than an error page.
async function withFallback<T>(read: () => Promise<T>, fallback: () => T): Promise<T> {
if (!isDatabaseConfigured) return fallback();
try {
return await read();
} catch (error) {
console.error("[content] database read failed, using fallback", error);
return fallback();
}
}Dynamic routes need at least one static param
With Cache Components, generateStaticParams must return at least one entry for a dynamic route so Next.js can validate the route at build time. We return all published slugs, with a placeholder if there are none yet; slugs published after the build render on first request and are cached from then on.
Results
- Public pages are static HTML at the edge, with no database query on most requests.
- Editors see changes immediately after saving — no redeploy, no waiting for a timer.
- The build and the live site survive database outages.
- Caching behavior is visible in code, not hidden in framework defaults.
Cache Components ask you to think about caching up front. In exchange, you get predictable performance and a codebase where every cached piece of data says exactly how fresh it is and what invalidates it.
_Need a fast, CMS-driven site or a Next.js migration? See web & app development._
Keep reading
All articles →Fixing INP: A Developer's Guide to Faster Interactions and Better Rankings
Interaction to Next Paint (INP) measures how quickly your site responds to taps, clicks and key presses. Here's how to find slow interactions and fix them in React and Next.js apps.
Prime Alley Microservices — One Product, Four Deployable Services
How we split billing, accounts, payments, and live updates into separate services while keeping a single login and navigation shell for end users.
Stripe Checkout in a Microservices Suite — The GSM Pattern
How GSM delivers a focused Next.js payment UI with Stripe Elements, Redis caching, and the Accounts API as the billing source of truth.