The solution to "the page is slow" is not just one. Vercel provides four cache layers, and you choose by "whole response vs. an individual data piece" and "framework integration vs. manual control." Mistake the layer and you either believe it's cached when it isn't (and lose on Active CPU billing and latency), or conversely have the accident of keeping serving stale data.
This article organizes the four layers with real code, faithful to the official specs of ISR, CDN Cache, and Runtime Cache. The deep dive into Next.js's Cache Components / PPR itself is left to the Next.js 16 App Router Cache Components article; this article concentrates on "how caching works as the Vercel platform." For the big picture, see the Vercel production-operations guide.
The map of the 4 layers: which to use first
| Layer | Target | Configuration | Persistence | Representative use |
|---|---|---|---|---|
| Static files | build output (JS/CSS/images/fonts) | automatic | deploy lifetime (persists across via hashed names) | every deploy. No need to think |
| ISR | pages (HTML + data) | framework API (revalidate, etc.) | 31 days, until revalidation | scheduled-update pages (EC, CMS, generated) |
| CDN Cache | a function's HTTP response | Cache-Control-family headers | up to 1 year (best effort) | cache API responses by region for non-ISR |
| Runtime Cache | a data piece inside a function | Runtime Cache API | until tag invalidation | reuse fetch results, DB queries, computed values |
A decision shortcut:
- A framework that emits a whole page (Next.js, etc.) → ISR (the strongest feature set comes automatically)
- Regionally cache an API's response → CDN Cache (Cache-Control)
- Fetch the same data repeatedly inside a function → Runtime Cache
- Static assets → do nothing (automatic)
ISR: the most feature-rich cache
stale-while-revalidate behavior
ISR is "return the cached immediately, regenerate behind." By riding on Vercel's CDN, the framework's ISR automatically gains the following (ISR docs).
- 31-day persistent storage: persistent in the Function region. An independent cache per deploy.
- 300ms global purge: on revalidation, all regions update within 300ms. HTML and data are swapped atomically (content doesn't conflict between full navigation and client navigation).
- Request coalescing: even if simultaneous access floods the same uncached path, the function is called only once per region. Origin protection during spikes.
- Instant-rollback resilience: a past deploy's cache isn't deleted, so even on rollback you don't lose generated content.
- Cache shield: on a CDN miss, the persistent ISR cache is read before calling the function.
// app/blog/[slug]/page.tsx
// 時間ベース再検証:3600秒ごとに裏で再生成
export const revalidate = 3600;
// 人気ページはビルド時に事前生成、それ以外はオンデマンド生成
export async function generateStaticParams() {
const popular = await getPopularSlugs();
return popular.map((slug) => ({ slug }));
}
export default async function Post({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <Article post={await getPost(slug)} />;
}
On-demand revalidation: reflect instantly on an event
"I don't want to wait 3600 seconds; I want it reflected the moment I publish in the CMS" — that's on-demand revalidation. Use revalidatePath / revalidateTag.
// app/api/revalidate/route.ts — CMS の Webhook から叩く
import { revalidatePath, revalidateTag } from "next/cache";
export async function POST(request: Request) {
// ① 認可(無防備な再検証エンドポイントは攻撃面になる)
if (request.headers.get("authorization") !== `Bearer ${process.env.REVALIDATE_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}
const { slug, tag } = await request.json();
if (slug) revalidatePath(`/blog/${slug}`); // パス単位
if (tag) revalidateTag(tag); // タグ単位(複数ページ横断)
return Response.json({ revalidated: true });
}
If you tag on the data-fetching side, you can invalidate cross-cuttingly like revalidateTag("posts").
// fetch にタグを付ける(Next.js)
const posts = await fetch("https://cms.example.com/posts", {
next: { tags: ["posts"], revalidate: 3600 },
}).then((r) => r.json());
What if revalidation fails? Vercel keeps returning stale and retries with a 30-second TTL. The statuses regarded as revalidation success are only
200/301/302/307/308/404/410. Anything else, network errors, and function errors are treated as failure. So "even if the revalidation target is temporarily down, users keep seeing the old page" — a design that helps availability.
Supported frameworks
ISR is not Next.js-only.
| Framework | Enabling |
|---|---|
| Next.js (App Router) | export revalidate from a route segment |
| Next.js (Pages Router) | getStaticProps returns revalidate |
| SvelteKit | an isr property in config |
| Nuxt | isr in routeRules |
| Astro | ISR settings in server output |
| Gatsby | DSG (Deferred Static Generation) |
Cache Components (PPR): having both a static shell and dynamic holes
Next.js 16's Cache Components (Partial Prerendering, PPR) is a model that "within one page, delivers the static shell immediately while punching holes only for the dynamic parts and streaming them." Declare the boundary with the use cache directive and control lifetime and tags with cacheLife / cacheTag.
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function Dashboard() {
return (
<>
<StaticHeader /> {/* 静的シェル:即配信 */}
<Suspense fallback={<Skel />}>
<UserStats /> {/* 動的な穴:ストリーム */}
</Suspense>
</>
);
}
// キャッシュ境界を宣言(コンポーネント/関数単位)
async function getCatalog() {
"use cache";
cacheLife("hours"); // 寿命プロファイル
cacheTag("catalog"); // タグ付け → updateTag/revalidateTag で無効化
return db.products.findMany();
}
On Vercel, this static shell rides on ISR/CDN, and the dynamic holes are filled by Fluid Compute functions via streaming. With per-tag invalidation (updateTag/revalidateTag), you can instantly update only the relevant tag after a write.
The details of Cache Components' APIs (
use cache,cacheLife,cacheTag,updateTag, migration fromunstable_cache) are consolidated in the frontend cluster's Next.js 16 Cache Components article. In this article, it's enough to grasp "how it's delivered on Vercel."
CDN Cache: control function responses with three headers
Even for APIs / SSR that don't use ISR, return Cache-Control and it's cached in that region's CDN. Vercel provides three headers that can separately control the browser / downstream CDN / Vercel CDN (CDN Cache docs).
// app/api/catalog/route.ts
export async function GET() {
return Response.json(await getCatalog(), {
headers: {
"Cache-Control": "public, max-age=10", // ブラウザ:10秒
"CDN-Cache-Control": "public, s-maxage=60", // 下流CDN:60秒
"Vercel-CDN-Cache-Control": "public, s-maxage=3600", // Vercel:3600秒
},
});
}
| Header | Controls | Returned to browser |
|---|---|---|
Cache-Control | browser + CDN (if no CDN-specific) | ✅ |
CDN-Cache-Control | downstream CDN, Vercel CDN (browser ignores) | ✅ |
Vercel-CDN-Cache-Control | Vercel CDN only | ❌ (not returned, not forwarded) |
To cache a function response in the CDN, you need s-maxage=N (optionally , stale-while-revalidate=Z). If you write only Cache-Control without specifying CDN-Cache-Control, Vercel strips s-maxage/stale-while-revalidate before sending to the browser (so as not to leak the internal cache policy to the browser).
Cacheable conditions (strict)
For a response to be cached in the CDN, it must satisfy all of these.
- The request is
GETorHEADand contains noRange/Authorizationheader - The response is
200/404/410/301/302/307/308 - Content length is 10MB or less (streaming is 20MB)
- Contains no
Set-Cookie Cache-Controlcontains noprivate/no-cache/no-store- Contains no
Vary: *
AuthorizationandSet-Cookieare the gatekeepers: they are the most common cause of an authenticated API not being cached. Don't cache login-required pages by default (or design withVaryand separation of user-independent parts).
Split by user attribute with Vary
When you want to vary content by country, language, or device while caching, use Vary. Vercel can add request headers like X-Vercel-IP-Country to the cache key.
export async function GET(request: Request) {
const country = request.headers.get("x-vercel-ip-country") ?? "unknown";
return Response.json({ greeting: `Hello from ${country}` }, {
headers: { "Cache-Control": "s-maxage=3600", Vary: "X-Vercel-IP-Country" },
});
}
Each added Vary exponentially increases cache entries (= lowers hit rate), so limit it to only the headers that genuinely change content.
Runtime Cache: cache a data piece inside a function
When you want to reuse "the same fetch, the same DB query, the same computed result" inside a function rather than the whole response. Runtime Cache is a per-region ephemeral KV that can be invalidated by tag and is shared across Functions, Routing Middleware, and Builds.
// 高コストな計算を Runtime Cache に載せる(疑似コード)
import { getCache } from "@vercel/functions";
export async function GET() {
const cache = getCache();
const key = "exchange-rates";
const cached = await cache.get(key);
if (cached) return Response.json(cached); // データ片の再利用
const rates = await fetchExchangeRatesFromUpstream(); // 高コスト
await cache.set(key, rates, { tags: ["rates"], ttl: 300 });
return Response.json(rates);
}
Whereas ISR and Cache-Control handle "the whole response," the essential difference is that Runtime Cache handles "an individual data piece." The two can be used together (the page with ISR, some fetch inside the page with Runtime Cache).
Debugging: read x-vercel-cache
The only way to prevent "you think it's cached but actually the function runs every time" is to actually look at the x-vercel-cache header.
curl -sI https://your-app.vercel.app/api/catalog | grep -i x-vercel-cache
# x-vercel-cache: HIT
| Value | Meaning |
|---|---|
HIT | served from CDN cache (the function didn't run) |
MISS | not in cache, generated at the origin (function) |
STALE | expired but stale served, revalidating behind |
PRERENDER | static content pre-generated at build time |
Before a production release, always confirm whether the path you want cached is HIT/PRERENDER. This directly sways Active CPU billing and latency (cost-optimization guide).
Production checklist (caching)
- You're intentionally choosing from the 4 cache layers (not vaguely SSR)
- ISR's
revalidateset to the requirement, popular pages withgenerateStaticParams - The on-demand revalidation endpoint is authorized
- The function cache has
s-maxageand you understand the role of the three headers - You're not accidentally caching authenticated responses (Authorization/Set-Cookie)
-
Varyis only the headers that genuinely change content - Measured-confirm
HIT/PRERENDERwithx-vercel-cache - Monitor CWV (LCP/INP/CLS) with Speed Insights (CWV optimization)
Conclusion
Caching creates not only "speed" but cost (not invoking Active CPU) and availability (stale delivery even on revalidation failure) at the same time.
- Choose the layer: ISR for pages, CDN Cache for APIs, Runtime Cache for data pieces, automatic for static
- Leverage ISR's automatic optimizations (31-day persistence, 300ms purge, request coalescing, rollback resilience)
- Reflect instantly with on-demand revalidation, and have both shell and holes with Cache Components
- Control layers with the three headers and satisfy the cacheable conditions
- Always measure with
x-vercel-cache
Next, on to the deploy / CI/CD / rollback guide for safely shipping what you built.
This article is based on the ISR / CDN Cache / Runtime Cache official documentation (as of June 2026). Specs are updated, so confirm the latest values in the official docs when adopting in production.