loke.dev
Layered translucent blocks represent shared cached data, a changing clock, and a dynamic user-specific request boundary.

Next.js 16 Cache Components: Choose cacheLife Before You Cache Data

Use a clear freshness contract for Next.js 16 Cache Components: decide what can be shared, set cacheLife deliberately, tag reads, and test invalidation.

Published By Loke6 min read

The short version

In Next.js 16, cache a value only when you can describe three things: who may share it, how stale it may be, and what event makes it invalid. Put that decision next to the code with use cache, cacheLife, and cacheTag. Do not treat a route-level revalidate number as a drop-in mental model.

This guide is for App Router projects using Next.js 16 with cacheComponents enabled. The feature changes the default: runtime data stays dynamic unless you explicitly cache a function, component, or route. That is a good default, but it makes vague caching decisions show up as stale UI or unnecessary database work.

The decision in one minute

Ask these questions before adding use cache:

1. Can the result be shared between different users? If it depends on a session, cookie, authorization result, cart, or per-user feature flag, do not put that value inside a shared cache.

2. Can a reader see an older result? If yes, name the permitted freshness window with cacheLife. If no, leave it dynamic or invalidate it synchronously after the write.

3. Is there a known write that changes the result? If yes, add a cacheTag at the read and invalidate that tag from the write path.

4. Is the entire route cacheable? Only put use cache at page level when every part of the output has the same sharing and freshness rule. Most real routes are clearer when caching a small data function instead.

What changed in Next.js 16

The Cache Components documentation describes the model: with cacheComponents enabled, runtime data cannot be used in the same scope as use cache. Cached work can contribute to the static shell; dynamic work belongs behind a dynamic boundary. The old route-wide controls are not the main tool for this model.

Next.js documents cacheLife as a lifetime for a cached function or component. Its stale value affects the client router cache, revalidate controls background server refresh, and expire is the maximum age before the next request must regenerate synchronously. Those are different clocks, so “one hour” is not enough as a design decision.

Start with a small cached read

A public product catalog is a good fit when every visitor may see the same result and a short delay is acceptable. Keep the cache boundary around the shared read, not around a page that also needs session state.

// lib/catalog.ts
import { cacheLife, cacheTag } from 'next/cache'

export async function getCatalog() {
  'use cache'
  cacheLife('hours')
  cacheTag('catalog')

  return db.product.findMany({
    where: { published: true },
    orderBy: { updatedAt: 'desc' },
  })
}

The example shares the catalog between requests. It says that the server may refresh it hourly and that a stale result can be used while refresh happens, according to the hours profile. If that is not acceptable for stock or price, use a shorter explicit policy or keep that field dynamic.

Do not put a user-specific query in this function. A query shaped by a user ID, private role, or request cookie needs a separate request-time boundary. You can still compose the cached catalog with a dynamic cart or account panel in the same route.

Choose a lifetime from the product rule, not a familiar number

Use the preset names only when their behavior matches the product. The official profiles are a useful starting point: seconds for rapidly changing values, hours for multiple daily changes, days for content that changes daily, and max for rare changes. Make an explicit custom profile when the business rule needs a different contract.

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    catalog: {
      stale: 60,
      revalidate: 300,
      expire: 3600,
    },
  },
}

export default nextConfig
// lib/catalog.ts
import { cacheLife } from 'next/cache'

export async function getCatalog() {
  'use cache'
  cacheLife('catalog')
  return db.product.findMany()
}

Here, client navigation can reuse a value for 60 seconds, the server starts a background refresh after five minutes, and the cache must not remain usable past one hour. Write those three numbers down in the pull request. They are a user-visible freshness policy, not a performance tweak.

Tag reads when a write has a clear invalidation event

Time-based refresh is not a replacement for a content update. If an admin publishing a product should update the catalog soon after the write, tag the shared read and revalidate the tag after a successful mutation.

// app/admin/actions.ts
'use server'

import { revalidateTag } from 'next/cache'

export async function publishProduct(input: ProductInput) {
  await requireAdmin()
  await saveProduct(input)

  revalidateTag('catalog', 'max')
}

The Next.js revalidation guide distinguishes revalidateTag from immediate-expiry behavior. The max profile marks tagged entries stale while they are refreshed, which is appropriate when brief eventual consistency is acceptable. If the next read must receive the new value, choose the stronger invalidation behavior documented for your version and test the write-to-read flow.

Common failures, and what they usually mean

The database still runs on every request. First check that use cache is at the top of the async function you are actually calling. Then check whether an uncached parent or a different request-time path is doing the query. Do not assume fetch revalidate settings control an arbitrary database client.

The UI stays old after an admin edit. Verify the read has the same tag you invalidate, the action only invalidates after the write succeeds, and your hosting platform supports the cache behavior you expect. Test with a real read after the mutation, not only a successful action response.

A build complains about cookies, headers, or time. A cached scope cannot directly read runtime data. Move the runtime read to the dynamic parent and pass only safe, serializable input to a cached function when that value is intended to create a separate cache entry.

A whole page becomes hard to explain. Pull use cache down to the data function. A page can combine a stable public section and a dynamic session-specific section without pretending they share one lifetime.

A version and deployment caveat

Cache Components was introduced in Next.js 16.0.0, according to the configuration reference. The Cache Components guide also says it requires the Node.js runtime and is not compatible with runtime = 'edge'. Confirm your exact Next.js minor version and hosting adapter before migrating a route.

The feature is still a model change, not a mechanical search and replace. A current Stack Overflow question about a cache function still hitting the database shows the language readers use when the boundary is unclear. Treat that as demand evidence, not proof of framework behavior. The implementation should follow the versioned Next.js documentation for the version you deploy.

A practical test checklist

1. Run the production build with cacheComponents enabled.

2. Load the page twice and log the cached data function so you can distinguish a request from a cache hit.

3. Navigate away and back within the stale window, then after it. Confirm the expected client behavior.

4. Perform the admin write, then load the public page. Confirm the tag and policy produce the freshness contract you promised.

5. Test logged-out and logged-in states. A private value must never appear because a shared cache entry was reused.

6. Run the same checks in the deployment runtime. Local development does not prove a distributed cache behaves like production.

Use cache to make a deliberate promise: this result is shareable, this is how long it can be old, and this is how it changes. When you cannot make that promise, leave the work dynamic. That is simpler than debugging a cache key you never intended to create.

Sources

Next.js: Cache Components

Next.js: cacheLife API reference

Next.js: Revalidating

Next.js: cacheComponents configuration

Stack Overflow: cache function still hits the database

Sources and further reading

  1. Cache Components · Next.js
  2. cacheLife API reference · Next.js
  3. Revalidating · Next.js
  4. cacheComponents configuration · Next.js