
Next.js cacheLife Function
A use cache directive tells Next.js that something should be cached. cacheLife tells it for how long, and — less obviously — how that duration affects whether the content can be prerendered at all. This is the deep, full API reference for cacheLife; the earlier "Caching" article on this blog covers the getting-started-level basics of the caching model as a whole, while this one goes into the timing semantics, preset profiles, and the genuinely subtle nested-caching behavior that trips people up once an app grows past a single isolated cached function.
Prerequisite: Enabling Cache Components
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
cacheLife only works within a cache directive's scope — inside a function or component marked with use cache (at the file level, or at the top of the function/component itself). Calling cacheLife at module scope, outside any such function, throws an error rather than silently doing nothing.
Basic Usage
"use cache";
import { cacheLife } from "next/cache";
export default async function BlogPage() {
cacheLife("days"); // Blog content updated daily
const posts = await getBlogPosts();
return <div>{/* render posts */}</div>;
}
If you don't call cacheLife at all, the default profile applies implicitly. The docs are explicit on this being a real recommendation, not just a style preference: set cacheLife explicitly in every use cache scope, so its behavior is legible at the call site rather than something you have to trace through nested caches to understand (more on exactly why this matters below, in the nested-caching section).
Two rules worth internalizing early: call cacheLife in the same function or component where the caching directive lives — don't abstract it into a shared utility, since that obscures the cache behavior from anyone reading the actual cached scope. And if you call it conditionally across different branches, ensure only one call executes per invocation — calling it multiple times per request isn't supported.
The Three Timing Properties
Every cache profile is built from three numbers, each controlling a genuinely different phase of the cache's lifecycle:
stale — Client-Side
How long the client-side router can serve cached content without any network request at all. During this window, navigation is instant, served entirely from the client cache — but the data may be outdated. Once this window closes, the next navigation has to check with the server.
cacheLife({ stale: 300 }); // 5 minutes
Omitted, this defaults to the default profile's value (5 minutes). It's also load-bearing for a second reason covered below: stale directly determines whether content can be part of the route's App Shell.
revalidate — Server-Side Background Refresh
How often the server regenerates cached content in the background, conceptually similar to Incremental Static Regeneration. Once a request arrives after this window: the server serves the existing cached version immediately, regenerates content in the background, and updates the cache with the fresh result for subsequent requests.
cacheLife({ revalidate: 900 }); // 15 minutes
Defaults to 15 minutes if omitted.
expire — The Hard Ceiling
The maximum time before the server must regenerate content synchronously — after this much time with zero traffic, the next request blocks on a fresh render rather than serving a background-refreshed stale copy. If you set both revalidate and expire, expire must be strictly longer than revalidate — Next.js validates this at build/runtime and errors on invalid configurations where that ordering is violated.
cacheLife({ expire: 3600 }); // 1 hour
Defaults to "never" if omitted.
Preset Profiles
Rather than hand-tuning all three numbers every time, Next.js ships preset profiles mapped to common content-freshness patterns:
| Profile | Use case | stale | revalidate | expire |
|---|---|---|---|---|
default | Standard content | 5 min | 15 min | never |
seconds | Real-time data | 30 sec | 1 sec | 1 min |
minutes | Frequently updated | 5 min | 1 min | 1 hour |
hours | Multiple daily updates | 5 min | 1 hour | 1 day |
days | Daily updates | 5 min | 1 day | 1 week |
weeks | Weekly updates | 5 min | 1 week | 30 days |
max | Rarely changes | 5 min | 30 days | 1 year |
Passing just the profile name is all that's required for the overwhelming majority of cached functions: cacheLife('hours'), cacheLife('days'), and so on.
Redefining Built-In Profiles
You can override any preset — including default and max — in next.config.ts:
const nextConfig = {
cacheComponents: true,
cacheLife: {
default: {
stale: 300,
revalidate: 3600, // now 1 hour instead of 15 minutes
expire: 86400,
},
},
};
export default nextConfig;
This is a genuinely supported pattern — but worth documenting deliberately in your project, since a call to cacheLife('hours') after redefinition reflects your values, not the preset's, and the whole point of a named profile is that it should mean what a reader expects. The docs specifically flag that redefining the time-named profiles (days, weeks, and similar) is riskier for exactly this reason — days carries an intuitive expectation of "roughly 24 hours," so quietly changing what it means is more likely to surprise a future reader than redefining default or max, which don't carry an inherent duration expectation in their names. If you need custom timing, defining an entirely new named profile is often the safer choice over silently redefining a built-in one.
One nice side effect worth knowing: because cacheLife's type signature is generated from your next.config.ts during dev/build/typegen, your editor's autocomplete and JSDoc hints for a redefined profile reflect the actual values you configured, not the stock preset documentation.
Custom Profiles
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
biweekly: {
stale: 60 * 60 * 24 * 14,
revalidate: 60 * 60 * 24,
expire: 60 * 60 * 24 * 14,
},
},
};
Referenced by name exactly like a built-in: cacheLife('biweekly'). Any property you omit from a custom profile inherits from default — this same inheritance rule also applies to inline profile objects passed directly to cacheLife().
Inline Profiles
For genuinely one-off timing needs that don't warrant a reusable named profile:
cacheLife({
stale: 3600,
revalidate: 900,
expire: 86400,
});
cacheLife({}) with a fully empty object applies the default profile's values wholesale.
Client Cache Behavior — a Detail Easy to Misread
stale controls the client-side router cache, communicated via the x-nextjs-stale-time response header — it is not the Cache-Control header, and conflating the two will lead you to the wrong mental model. A hard-enforced minimum of 30 seconds exists specifically so a prefetched link's data doesn't expire before a user has had a realistic chance to actually click it — this floor only applies to time-based expiration.
Separately: calling any revalidation function from a Server Action (revalidateTag, revalidatePath, updateTag, refresh) immediately clears the entire client cache, bypassing stale entirely — a mutation always wins over a stale-time window, regardless of how long that window had left.
Worth distinguishing explicitly: cacheLife's stale is a per-function or per-route setting; staleTimes in next.config.js is the equivalent global setting affecting every route. Updating staleTimes.static also updates the default profile's stale value — the two aren't entirely independent of each other.
Prerendering Behavior — the Section With Real Architectural Consequences
This is where cacheLife's timing values stop being purely about freshness and start determining where content can physically be served from:
revalidateof0, orexpireunder 5 minutes → excluded from prerendering entirely, becoming a "dynamic hole" resolved at request time.staleunder 30 seconds → also excluded from prerendering — a prefetch that would expire before a user could plausibly click it isn't worth prerendering in the first place.staleof at least 30 seconds but under 5 minutes → included in prerendering, but excluded from the route's App Shell specifically.
Of all the presets, only seconds crosses any of these thresholds (its 1-minute expire excludes it from prerendering) — every other preset is prerender-eligible by design. This is precisely how Cache Components lets you mix static and dynamic content on the same page: static parts get prerendered; short-lived cached regions become request-time boundaries, wrapped in <Suspense> for a fallback while the fresh content resolves.
Nested Caching: The Behavior Most Worth Understanding Deeply
When a cached function/component calls into another cached function/component, the outer cache's actual behavior depends entirely on whether it has an explicit cacheLife.
With an explicit outer cacheLife: the outer cache always uses its own configured lifetime, regardless of what any nested cache specifies — longer or shorter, it doesn't matter, the explicit value always wins for the outer scope. When the outer cache hits, it returns its complete output, nested data included.
Without an explicit outer cacheLife: the outer cache falls back to the default profile (15-minute revalidate) — but a nested cache with a shorter lifetime can silently pull that default down, while a nested cache with a longer lifetime cannot extend it beyond the default. This asymmetry — shorter propagates up, longer doesn't — is exactly why the docs recommend always setting cacheLife explicitly: without it, a function's actual caching behavior depends on what's nested inside it, which you can't know just from reading that one function in isolation.
The Build Error That Protects You From a Real Footgun
If a short-lived nested cache (recall: zero revalidate, or expire under 5 minutes) sits inside an outer use cache scope that has no explicit cacheLife, the outer scope's lifetime would silently inherit that short duration through propagation — and the nested cache causing this might not even be visible in the file you're looking at; it could be buried inside an imported module or a third-party dependency. To prevent this from silently corrupting your caching strategy, Next.js throws an error during prerendering rather than letting it happen quietly:
export async function ShortLivedWidget() {
"use cache";
cacheLife("seconds");
const data = await fetchRealtimeData();
return <div>{data}</div>;
}
export default async function Page() {
"use cache";
// Error: no explicit cacheLife on this outer cache
return (
<div>
<ShortLivedWidget />
</div>
);
}
Two ways to fix it, depending on which behavior you actually want:
Keep the outer cache static by giving it its own explicit, longer lifetime:
export default async function Page() {
"use cache";
cacheLife("default"); // explicit — prevents the error
return (
<div>
<ShortLivedWidget />
</div>
);
}
Confirm the outer cache should also be short-lived, explicitly, wrapped in <Suspense> so the rest of the page can still stream around it:
import { Suspense } from "react";
async function Content() {
"use cache: remote";
cacheLife("seconds"); // explicit — confirms this is intentional
return <ShortLivedWidget />;
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Content />
</Suspense>
);
}
Note the "use cache: remote" in that second example — the docs specifically flag this over plain "use cache" because runtime caching in serverless deployments doesn't persist across requests with the default in-memory cache; a self-hosted setup may be fine with plain "use cache" instead.
Conditional and Data-Driven Lifetimes
cacheLife can be called conditionally, letting different code paths express genuinely different caching needs — a missing/unpublished resource cached briefly to reduce repeated database load, versus a published one cached much longer:
async function getPostContent(slug: string) {
"use cache";
const post = await fetchPost(slug);
cacheTag(`post-${slug}`);
if (!post) {
cacheLife("minutes"); // may become available soon — check back sooner
return null;
}
cacheLife("days"); // published content changes rarely
return post.data;
}
And for genuinely data-driven timing — say, a CMS field controlling its own revalidation window — an inline profile object works with values computed at runtime:
cacheLife({
revalidate: post.revalidateSeconds ?? 3600,
// stale and expire inherit from 'default'
});
Version History
cacheLife and its surrounding Cache Components model are part of the current App Router caching architecture — check the "Caching" and "use cache" reference articles on this blog for the broader model's own version history if you're tracking when specific pieces of this system stabilized.
Key Takeaways
| Concept | Detail |
|---|---|
| Scope requirement | Only usable inside a use cache scope — errors at module scope |
stale | Client-side cache window; 30-second minimum enforced |
revalidate | Server background-refresh interval |
expire | Hard ceiling forcing synchronous regeneration; must exceed revalidate if both are set |
| Prerender eligibility | Short stale/revalidate/expire values exclude content from the static shell or prerendering entirely |
| Nested caching | An outer cache without explicit cacheLife inherits shorter (but not longer) nested lifetimes |
| Build-time protection | A short-lived nested cache inside an unconfigured outer cache throws a build error, not a silent misconfiguration |
The single habit worth taking from this entire reference: always set cacheLife explicitly, even when the default would technically work. It's the difference between a caching strategy you can read directly off the code, and one you can only understand by tracing every nested cache call it happens to contain.


