Designing Cache Strategies by Service Type with Next.js 15 Custom `cacheLife` Profiles and Vercel Edge Cache Layering
News feeds, marketing landing pages, real-time inventory screens — how are you caching all three within a single service? You've likely encountered situations where a product price updated right after deployment stays stale for over 30 minutes, or conversely, a category list that barely ever changes hits the database on every request. When you scatter fetch() options or revalidate numbers across individual files, it becomes nearly impossible to understand "what's being cached, where, and for how long."
The use cache directive and cacheLife() API introduced in Next.js 15 take a fundamentally different approach to this problem. Once you define cache profiles by service type in next.config.ts, you can declare a consistent cache policy in each component using just a single name. Add Vercel's layered caching headers on top, and you can independently tune each layer — browser, global edge CDN, and server cache.
This article is aimed at frontend developers with Next.js App Router experience. It walks through, step by step, how to design custom cacheLife profiles suited to the nature of your service data, and how to combine Vercel edge cache and server cache in layers to always deliver fast responses to users.
Experimental Feature Notice: The
use cachedirective is an experimental feature as of Next.js 15.x. It is strongly recommended to check the latest Next.js release notes and official documentation before applying it to production.
Core Concepts
The Three Time Axes of a cacheLife Profile
A profile passed to cacheLife() consists of three numeric properties, each responsible for a different stage of the cache lifecycle.
| Property | Role | Analogy |
|---|---|---|
stale |
How long the client uses the cache as-is without checking the server | Grabbing food straight from the fridge |
revalidate |
How often the cache is quietly refreshed in the background | Going grocery shopping while you sleep |
expire |
After this time with no requests, the cache is discarded entirely and falls back to dynamic fetching | Expiration date |
Note:
expiremust always be greater thanrevalidate. Violating this constraint causes a build-time error in Next.js. Using the formulaexpire = revalidate × Nas a baseline keeps things safe.
stale-while-revalidate: An HTTP caching strategy where, even after a cache entry has expired, the previous response is returned immediately while new data is fetched in the background simultaneously. Users always get a fast response, and the new data takes effect starting from the next request. The window betweenrevalidateandexpireis precisely this "stale but fast" response period.
Vercel Layered Cache Headers
In the Vercel environment, three Cache-Control-family headers allow you to independently control each cache layer.
Cache-Control → Browser cache (the baseline for all HTTP caches)
CDN-Cache-Control → All intermediate CDNs, including Vercel
Vercel-CDN-Cache-Control → Vercel edge network only (highest priority)The actual request flow looks like this. In the Vercel environment, the Vercel Edge CDN sits directly in front of the origin server without any separate external CDN.
User Browser (governed by Cache-Control)
↓ on cache miss
Vercel Edge CDN (Vercel-CDN-Cache-Control takes priority, CDN-Cache-Control as fallback)
↓ on cache miss
Next.js Server Cache (governed by use cache + cacheLife profiles)
↓ on cache miss
Database / External APIISR (Incremental Static Regeneration): A cache layer maintained server-side by Next.js. Pages or data are generated and stored on the first request, and subsequent requests return the cached result while revalidation happens in the background. The
use cachedirective andcacheLifeprofiles provide fine-grained control over how this ISR server cache behaves.
When Vercel-CDN-Cache-Control is present, the Vercel edge reads only that header and ignores CDN-Cache-Control and Cache-Control at the edge level. This makes it possible to run the browser and the edge on completely different TTLs.
Practical Application
Example 1: Defining Custom Profiles by Service Type
Declare profiles that reflect your service domain in the cacheLife object inside next.config.ts. Enabling the cacheComponents: true option alongside this allows the use cache directive to be applied not just to functions, but also at the Server Component file level.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// Enable use cache at the Server Component file and function level
cacheComponents: true,
cacheLife: {
// News & blog: content changes frequently and requires fast updates
editorial: {
stale: 600, // 10 minutes — client cache
revalidate: 3600, // 1 hour — background refresh
expire: 86400, // 24 hours — maximum lifetime
},
// Marketing landing pages: low change frequency favors long TTLs
marketing: {
stale: 3600, // 1 hour
revalidate: 86400, // 24 hours
expire: 604800, // 7 days
},
// Real-time inventory & pricing: stale data directly causes business loss
inventory: {
stale: 60, // 1 minute
revalidate: 300, // 5 minutes
expire: 600, // 10 minutes
},
// Shared non-user-specific data like category lists
reference: {
stale: 86400, // 24 hours
revalidate: 604800, // 7 days
expire: 2592000, // 30 days
},
},
}
export default nextConfigHere's a summary of what data each profile is suited for:
| Profile | Target Data | Core Design Intent |
|---|---|---|
editorial |
Blog posts, news feeds | Stay current with hourly background refreshes |
marketing |
Hero banners, promotional copy | Maximize CDN hit rate with a 7-day TTL |
inventory |
Stock quantities, prices | Minimize stale data exposure with 5-minute refreshes |
reference |
Categories, country codes | Eliminate DB load with a 30-day TTL |
Example 2: Declaring Profiles in Components
Let's see how to apply the profiles defined above to actual Server Components. Declare 'use cache' at the top of each component and specify the profile name with cacheLife().
import { cacheLife, cacheTag } from 'next/cache'
// Blog post list — editorial profile: background refresh every 1 hour
export async function BlogList() {
'use cache'
cacheLife('editorial')
cacheTag('posts') // Grouped under 'posts' tag → can be instantly invalidated with revalidateTag('posts')
// fetchPosts() is a regular async function that retrieves the post list from the DB
// It's the return value (JSX) of BlogList that gets cached, not the function itself
const posts = await fetchPosts()
return <PostGrid posts={posts} />
}
// Marketing hero section — marketing profile: retained for 7 days
export async function HeroSection() {
'use cache'
cacheLife('marketing')
cacheTag('cms', 'hero')
// fetchCMSContent() is a regular async function that calls an external CMS API
const content = await fetchCMSContent('hero')
return <Hero content={content} />
}
use cachedirective: When declared at the top of a file, it applies to the entire file. When declared inside a function, only that function's return value is cached. The syntax mirrors React's'use client'.cacheComponents: truemust be enabled for component-level application to work.
By assigning tags with cacheTag(), you can instantly invalidate a group's cache simply by calling revalidateTag('posts') from a CMS webhook or admin API. Here's a simple example of a webhook receiver endpoint:
// app/api/revalidate/route.ts — CMS webhook receiver endpoint
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
const { tag } = await req.json() // e.g., { tag: 'posts' }
revalidateTag(tag)
return Response.json({ revalidated: true })
}Example 3: Combining Vercel Edge Cache and Server Cache Layers
Here we look at how to set the Vercel-CDN-Cache-Control header in a Route Handler to leverage both the Vercel edge CDN cache and the Next.js server cache simultaneously. Route Handlers are the right choice when external clients (mobile apps, other services) need to consume data directly, or when you need direct control over response headers.
// app/api/products/route.ts
import { cacheLife, cacheTag } from 'next/cache'
async function getProducts() {
'use cache'
cacheLife('inventory') // Server cache: background refresh every 5 minutes
cacheTag('products')
// Actual logic to fetch product data from DB or external API
return await db.products.findMany({ where: { active: true } })
}
export async function GET() {
const data = await getProducts()
return new Response(JSON.stringify(data), {
headers: {
// Browser: 1-minute cache
'Cache-Control': 'public, max-age=60',
// All CDNs: 5-minute cache, stale allowed for 10 minutes after
'CDN-Cache-Control': 's-maxage=300, stale-while-revalidate=600',
// Vercel edge only: aligned with server cache revalidate (300) to prevent mismatch
'Vercel-CDN-Cache-Control': 's-maxage=300, stale-while-revalidate=600',
},
})
}Here's a summary of which layer each header affects:
| Header | Layer Applied | Priority |
|---|---|---|
Cache-Control |
Browser | Low |
CDN-Cache-Control |
All CDNs (including Vercel) | Medium |
Vercel-CDN-Cache-Control |
Vercel edge only | Highest |
Design Caution: It is recommended to set
Vercel-CDN-Cache-Control'ss-maxageto match or be less than the server cache'srevalidatevalue. For example, if the edge is set to 1 hour (s-maxage=3600) but the server cache is set to 5 minutes (revalidate: 300), the server data refreshes every 5 minutes while the edge keeps returning the old response for a full hour. For freshness-critical data like inventory and pricing, this mismatch can translate directly into business loss.
Pros and Cons
Advantages
| Item | Detail |
|---|---|
| Fine-grained control | Cache policies can be separated at the component/function level, preventing both over-caching and cache absence simultaneously |
| Improved DX | Configuration is centralized in next.config.ts, making intent clear with just a profile name |
| Layer separation | Browser, Vercel Edge, and ISR server can be controlled independently for per-layer optimization |
| stale-while-revalidate | Users always get fast responses since the previous cache is returned even while a refresh is in progress |
| Instant invalidation | The cacheTag() + revalidateTag() combination lets you refresh a cache immediately without waiting for time-based expiry |
Disadvantages and Caveats
| Item | Detail | Mitigation |
|---|---|---|
expire > revalidate enforced |
Incorrect ordering causes a build error | Apply the expire = revalidate × N formula |
| Minimum 30-second stale enforced | Even setting stale: 0, the client router won't go below 30 seconds |
Exclude data requiring instant reflection from caching |
| Side effect caution | use cache caches only the return value, not the function execution itself |
Handle side effects like logging and event emission outside the cache boundary |
| No personalized data | Personal user data leaking into edge cache risks information exposure | Exclude components depending on session or user ID from use cache |
| Vercel-specific header | Vercel-CDN-Cache-Control only works on the Vercel platform |
Replace with CDN-Cache-Control or apply conditionally for multi-platform deployments |
| Experimental feature | use cache is still experimental as of Next.js 15.x |
Checking release notes before production use is mandatory |
Most Common Mistakes in Practice
- Applying
use cacheto personalized data — Logged-in users' cart contents, profile information, etc. must always be excluded from caching. This can lead to a severe information leak where the edge cache returns User A's data to User B. - Setting edge cache TTL much longer than the server cache
revalidate— Even when data is refreshed on the server side, the edge keeps returning the old response. It is recommended to align the two values or set the edge TTL shorter. - Relying solely on time-based expiry without
revalidateTag()— Services that require immediate content reflection after editing need to wire CMS webhooks torevalidateTag(). Otherwise, edits won't appear until the nextrevalidatecycle comes around.
Closing Thoughts
When this article began, the question was "how long should inventory data be cached?" — but looking back, it was really a more fundamental question. Cache policy is not a technical setting; it is a way of expressing the business nature of your data in code, and cacheLife profiles along with Vercel layered caching headers are the tools that make that expression systematic.
Here are 3 steps you can start with right now:
- Try classifying your service's data by type. Data that changes occasionally, like notices, maps to
editorial; data that changes frequently, like product prices and stock, maps toinventory; data that almost never changes, like categories and country codes, maps toreference. Define these innext.config.ts. - Enable
cacheComponents: true, then declare'use cache'andcacheLife(profileName)on your single most expensive Server Component. You'll be able to clearly see which components are cached and for how long. - Group related components with
cacheTag(), then connectrevalidateTag()calls from your CMS save hook or admin API. This enables immediate content updates without waiting for time-based expiry.
Next Article: On-demand cache invalidation strategy by integrating CMS webhooks with
cacheTag()+revalidateTag()— going beyond the limits of time-based expiry to reflect edits instantly
References
For Conceptual Understanding
- use cache Directive | Next.js Official Docs
- CDN Caching Guide | Next.js Official Docs
- Cache-Control Headers | Vercel Official Docs
- Cache Components for Instant and Fresh Pages | Vercel Academy
Configuration Reference
- cacheLife Function | Next.js Official Docs
- next.config.js cacheLife Configuration | Next.js Official Docs
- cacheComponents Configuration | Next.js Official Docs
- cacheHandlers Configuration | Next.js Official Docs
- CDN Cache | Vercel Official Docs
Further Learning