skip to content

In Next.js, what does calling `cacheLife()` from `next/cache` inside a function marked `'use cache'` control, and what do the `stale`, `revalidate` and `expire` values of a cache profile mean?

level: middleimportance: should knowfreq 34%

answer

  1. three numbers, three questions
  2. stale is client-side reuse
  3. revalidate refreshes in the background
  4. expire is the hard ceiling
  5. name the profile, don't scatter numbers

basics

~20 s

cacheLife() sets the lifetime profile of a 'use cache' entry. Its three values separate concerns: stale is how long a client may reuse the value without re-checking, revalidate is how often the server refreshes it in the background, and expire is the maximum age past which the value can no longer be served.

solid answer

~50 s

A bare `'use cache'` uses a default lifetime; `cacheLife()` overrides it for that entry, either with a named profile such as `'hours'` or with an inline object. The three numbers answer three different questions. `stale` is client-side: how long a client may keep using the value before it checks with the server again. `revalidate` is server-side: how frequently the entry is refreshed in the background while the existing value keeps being served, which is the stale-while-revalidate behaviour. `expire` is the hard ceiling: once an entry is that old it is no longer servable, so the next request has to wait for fresh work. Interviewers ask this because candidates collapse all three into "the TTL" — but a profile like `{ stale: 60, revalidate: 300, expire: 3600 }` deliberately says "cheap for a minute, refresh every five, never serve anything older than an hour". Custom named profiles can be declared in `next.config` and referenced by name.

code

typescript · 10 lines
typescript
import { cacheLife, cacheTag } from 'next/cache'

export async function getPost(slug: string) {
  'use cache'
  cacheLife({ stale: 60, revalidate: 300, expire: 3600 })
  cacheTag(`post:${slug}`)

  const res = await fetch(`https://api.example.com/posts/${slug}`)
  return res.json()
}

go deeper

for a junior

Know that a cached function's lifetime is set by calling cacheLife() with a named profile such as 'hours', not by writing timing logic inside the function.

for a middle

Be able to separate the three values out loud: client reuse window, background refresh interval, and the hard age limit past which nothing may be served.

for a senior

Show you pick these numbers from the data's real change rate and the cost of staleness, and that you pair a generous lifetime with tag-based invalidation rather than defending with a short TTL.

for a principal

Own the policy layer: named profiles declared once and applied by data class, so freshness is a reviewable decision per dataset instead of a number a developer guessed at a call site.

## Why one number is not enough A naive cache has one knob: time to live. That forces a single answer to three questions that in practice have different answers — how eagerly should a client re-check, how often should the server do the work again, and at what point does a value become unacceptable to serve at all. `cacheLife()` splits them. ```ts import { cacheLife } from 'next/cache' export async function getHomepageFeed() { 'use cache' cacheLife('hours') const res = await fetch('https://api.example.com/feed') return res.json() } ``` In earlier Next 15 canaries this helper was exported as `unstable_cacheLife`; the shape of what it controls did not change. ## The three values **`stale`** governs the client side. It is how long a client may reuse a value it already has without asking the server whether anything changed. Raising it makes repeat navigation feel instant and cuts server traffic; the cost is that a user can look at old content for that long even after the server has fresher data. **`revalidate`** governs the server side. It is the interval at which the entry is refreshed in the background. Crucially, the refresh does not block anyone: requests keep being served the existing value while the new one is computed, then later requests get the new value. This is the stale-while-revalidate shape — nobody waits, the data is at most one interval behind. **`expire`** is the hard limit. Past this age the value is no longer allowed to be served at all, so the next request has to block on real work. It is your protection against an endpoint that stops being requested, or a background refresh that keeps failing, quietly serving something from last week. A profile therefore reads as a policy sentence: `{ stale: 60, revalidate: 300, expire: 3600 }` means "clients may reuse for a minute, the server refreshes every five minutes behind the scenes, and nothing older than an hour is servable". ## Named profiles Rather than sprinkling raw numbers through the codebase, `cacheLife()` also takes a named profile string — Next ships coarse ones such as `'seconds'`, `'hours'` and `'max'` — and you can declare your own in `next.config` and refer to them by name: ```ts export async function getPost(slug: string) { 'use cache' cacheLife('blog') // a profile declared once in next.config return db.post.findUnique({ where: { slug } }) } ``` This is worth doing for the same reason design tokens are: the name carries the intent (`'blog'`, `'pricing'`, `'nav'`), and when the business changes its mind about freshness you edit one profile rather than hunting for the number 300. ## Where it sits relative to other invalidation `cacheLife()` is *time-based* policy: it decides when an entry ages out on its own. It composes with, and does not replace, event-based invalidation — `cacheTag()` labels an entry so it can be thrown away deliberately when the underlying data actually changes. A typical cached loader carries both: a generous lifetime so the common path is cheap, plus a tag so a write path can invalidate it immediately. Without the tag you are stuck choosing a short lifetime purely as insurance, which is how teams end up with a cache that never hits. ## Common mistakes - **Treating all three as the same thing.** Setting `stale` equal to `expire` throws away the whole point of a background refresh window. - **Trying to implement freshness inside the function body.** Comparing `Date.now()` against a stored timestamp does not work: the timestamp is part of the stored value and gets replayed. Lifetime lives in the profile, not in the body. - **Very short lifetimes everywhere.** An entry that expires faster than the traffic arrives has a hit rate near zero — you have paid for a cache and kept the latency. - **Assuming a fixed default.** Caching defaults have moved between Next major versions, so quote the profile you set rather than relying on whatever the framework does when you say nothing.

  • What does `cacheTag()` add on top of `cacheLife()` for a cached Next.js function?
    `cacheLife()` is time-based ageing; `cacheTag()` labels the entry so it can be discarded on demand when the data actually changes, rather than waiting for a clock. Carrying both lets you set a generous lifetime for the common path while still invalidating immediately on a write, instead of using a short lifetime as insurance.
  • Why does implementing freshness by comparing `Date.now()` inside the cached function fail?
    Whatever the body computes, including the timestamp, is what gets stored and replayed. On a cache hit the body never runs, so the comparison is against a frozen value and always agrees with itself. Freshness has to be expressed in the profile, which the cache layer evaluates outside the function.
  • What is the risk of setting a very long `expire` on a frequently-changing dataset?
    If the background refresh path fails — the upstream is down, a deploy broke the query — the old value stays servable for as long as `expire` allows, so users see stale content with no error surfacing. A tighter `expire` converts a silent staleness bug into a visible slow request or failure you can alert on.

saying these in an interview costs you the question

  • Treats stale, revalidate and expire as one TTL
  • Thinks revalidate makes the request block until fresh
  • Sets freshness with Date.now() inside the function body
  • Assumes there is one universal default lifetime
  • Uses very short lifetimes everywhere and gets no cache hits

context