cacheLife()

Set how long a 'use cache' scope stays fresh, stale and valid, by profile or inline object.

Signature
cacheLife(profile: string | { stale?: number; revalidate?: number; expire?: number }): void

Parameters

ParameterTypeDescription
$profilestring | { stale?: number; revalidate?: number; expire?: number }A built-in profile name (default, seconds, minutes, hours, days, weeks, max), a custom profile defined under cacheLife in next.config, or an inline object of times in seconds. Omitted properties inherit from the default profile, and cacheLife({}) applies the default profile outright.
$profile.staleoptionalnumber= 300Seconds the client router may serve cached content without checking the server. Defaults to the default profile's 5 minutes; the router enforces a 30-second minimum so prefetched links stay usable.
$profile.revalidateoptionalnumber= 900Seconds after which the server regenerates the entry in the background, serving the cached version meanwhile. Defaults to the default profile's 15 minutes.
$profile.expireoptionalnumberMaximum seconds before the server must regenerate synchronously on the next request. Must be longer than revalidate. Defaults to the default profile's value, which never expires.

Return value

voidNothing. The enclosing cache scope adopts the given lifetime.

Errors

Throws when called at module scope or outside a cache directive scope, when expire is shorter than revalidate, and during prerendering when a short-lived nested cache is used from an outer 'use cache' that sets no explicit cacheLife.

cacheLife answers "how long?" for a 'use cache' scope. The three numbers are separate audiences: stale is the client router, revalidate is the background refresh on the server, and expire is the point where a request must wait for fresh content. Requires cacheComponents in next.config.

Set it every time

Omitting it applies the default profile, and the lifetime stops being visible at the call site. Worse, an outer scope without an explicit cacheLife inherits downward: a nested cache with a shorter life drags the outer one down, while a longer one cannot extend it past the default. With an explicit profile you can read one function and know its behavior without tracing the tree.

What trips people up

  • Short lifetimes change where content is served from. A revalidate of 0 or an expire under 5 minutes drops the scope out of prerendering into a request-time hole; a stale under 30 seconds does the same, because a prefetch would expire before the click. Of the presets only seconds crosses a threshold.
  • Only one call may execute per invocation — branch it, do not stack it.
  • Redefining a built-in name in next.config is supported, but then cacheLife('hours') no longer means hours to the next reader.

Example

Example · typescript
import { cacheLife } from 'next/cache'

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  'use cache'
  cacheLife('days') // Blog posts update daily

  const { slug } = await params
  const post = await fetchBlogPost(slug)

  return <article>{post.content}</article>
}

When to use it

  • Give blog pages a daily refresh with cacheLife('days') while product pages use 'hours'.
  • Cache a draft-or-missing record briefly and a published one for days, by branching the call.
  • Read a revalidate window out of CMS data and pass it as an inline profile object.

Frequently asked questions

What is the difference between stale, revalidate and expire?

stale is how long the client router serves its cached copy without contacting the server, revalidate is how often the server regenerates the entry in the background while still serving the cached one, and expire is the ceiling after which the next request waits for fresh content. expire must be longer than revalidate or Next.js raises an error.

What happens if I don't call cacheLife?

The default profile applies: 5 minutes stale, 15 minutes revalidate, never expires. The lifetime also stops being explicit, so a nested cache with a shorter life can shorten the outer scope, and nesting a short-lived cache inside one with no explicit cacheLife fails the build during prerendering.

Why does my cached page render at request time instead of being prerendered?

Its lifetime crossed a prerender threshold: a revalidate of 0 or an expire under 5 minutes turns the scope into a request-time hole, and a stale under 30 seconds does too because a prefetch would expire before the user could click. Among the presets, only 'seconds' falls below one.

More examples

Built-in profiles

Lives at lib/data.ts. The table is the whole decision: pick the row whose revalidate matches how often the content actually changes. 'seconds' is the one preset whose expire (1 minute) is short enough to exclude it from prerenders, so it always resolves at request time.

Example · typescript
// Profile   stale       revalidate   expire
// default   5 minutes   15 minutes   never
// seconds   30 seconds  1 second     1 minute
// minutes   5 minutes   1 minute     1 hour
// hours     5 minutes   1 hour       1 day
// days      5 minutes   1 day        1 week
// weeks     5 minutes   1 week       30 days
// max       5 minutes   30 days      1 year

import { cacheLife } from 'next/cache'

export async function getSettings() {
  'use cache'
  cacheLife('max') // Settings rarely change
  return fetchSettings()
}

export async function getRealtimeStats() {
  'use cache'
  cacheLife('seconds') // Stats update constantly
  return fetchStats()
}

Custom and inline profiles

Lives at next.config.ts. Naming a profile once means cacheLife('editorial') reads the same everywhere and changes in one place. Any property you leave out inherits from the default profile — the same rule applies to inline objects passed straight to cacheLife().

Example · typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    editorial: {
      stale: 600, // 10 minutes
      revalidate: 3600, // 1 hour
      expire: 86400, // 1 day
    },
  },
}

export default nextConfig

Branching on the data

Lives at lib/posts.ts. Two calls appear in the source but only one runs per invocation, which is the rule. A missing record gets a short life because it is likely to appear soon; a published one gets a long life plus a tag for on-demand invalidation.

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

export async function getPostContent(slug: string) {
  'use cache'

  const post = await fetchPost(slug)
  cacheTag(`post-${slug}`)

  if (!post) {
    // Might be unpublished; cache briefly to spare the database
    cacheLife('minutes')
    return null
  }

  cacheLife('days')
  return post.data
}

Discussion

  • Be the first to comment on this lesson.