Skip to main content
友田 陽大
Vercel in production
Vercel
Next.js
パフォーマンス
ISR
キャッシュ
コスト最適化
フロントエンド

Vercel caching-strategy guide: using the 4 layers of ISR, CDN Cache, Runtime Cache, and Cache Components (PPR)

A caching implementation guide faithful to the Vercel official docs. With real code, it explains the four layers — static cache, ISR (stale-while-revalidate / 31-day persistence / 300ms global purge / request coalescing), CDN Cache (the three headers Cache-Control / CDN-Cache-Control / Vercel-CDN-Cache-Control), Runtime Cache, and Cache Components (PPR) — through on-demand revalidation (revalidatePath/revalidateTag/updateTag) and reading x-vercel-cache.

Published
Reading time
9 min read
Author
友田 陽大
Share

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

LayerTargetConfigurationPersistenceRepresentative use
Static filesbuild output (JS/CSS/images/fonts)automaticdeploy lifetime (persists across via hashed names)every deploy. No need to think
ISRpages (HTML + data)framework API (revalidate, etc.)31 days, until revalidationscheduled-update pages (EC, CMS, generated)
CDN Cachea function's HTTP responseCache-Control-family headersup to 1 year (best effort)cache API responses by region for non-ISR
Runtime Cachea data piece inside a functionRuntime Cache APIuntil tag invalidationreuse 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 responseCDN 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.

FrameworkEnabling
Next.js (App Router)export revalidate from a route segment
Next.js (Pages Router)getStaticProps returns revalidate
SvelteKitan isr property in config
Nuxtisr in routeRules
AstroISR settings in server output
GatsbyDSG (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 from unstable_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秒
    },
  });
}
HeaderControlsReturned to browser
Cache-Controlbrowser + CDN (if no CDN-specific)
CDN-Cache-Controldownstream CDN, Vercel CDN (browser ignores)
Vercel-CDN-Cache-ControlVercel 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 GET or HEAD and contains no Range/Authorization header
  • The response is 200/404/410/301/302/307/308
  • Content length is 10MB or less (streaming is 20MB)
  • Contains no Set-Cookie
  • Cache-Control contains no private/no-cache/no-store
  • Contains no Vary: *

Authorization and Set-Cookie are 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 with Vary and 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
ValueMeaning
HITserved from CDN cache (the function didn't run)
MISSnot in cache, generated at the origin (function)
STALEexpired but stale served, revalidating behind
PRERENDERstatic 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 revalidate set to the requirement, popular pages with generateStaticParams
  • The on-demand revalidation endpoint is authorized
  • The function cache has s-maxage and you understand the role of the three headers
  • You're not accidentally caching authenticated responses (Authorization/Set-Cookie)
  • Vary is only the headers that genuinely change content
  • Measured-confirm HIT/PRERENDER with x-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.

  1. Choose the layer: ISR for pages, CDN Cache for APIs, Runtime Cache for data pieces, automatic for static
  2. Leverage ISR's automatic optimizations (31-day persistence, 300ms purge, request coalescing, rollback resilience)
  3. Reflect instantly with on-demand revalidation, and have both shell and holes with Cache Components
  4. Control layers with the three headers and satisfy the cacheable conditions
  5. 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.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

I can take on the implementation from this article as an engagement

Vercel apps, from design to production and cost optimization

Function design assuming Fluid Compute (safe global state, waitUntil, Cron), four-layer caching (ISR/CDN/Runtime Cache/Cache Components), safe deploys (preview/Promote/Instant Rollback/Rolling Releases), entry-point defense (Firewall/WAF/BotID), storage selection (Blob/Edge Config/Marketplace), and Active-CPU-billing-aware cost optimization. With experience running Next.js products on Vercel in production, I deliver fast, cheap, and secure.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading