# 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: 2026-06-28
- Author: 友田 陽大
- Tags: Vercel, Next.js, パフォーマンス, ISR, キャッシュ, コスト最適化, フロントエンド
- URL: https://tomodahinata.com/en/blog/vercel-caching-isr-cache-components-ppr-guide
- Category: Vercel in production
- Pillar guide: https://tomodahinata.com/en/blog/vercel-production-platform-guide

## Key points

- Vercel's cache has 4 layers — 'static (automatic) / ISR (framework integration) / CDN Cache (Cache-Control header) / Runtime Cache (in-function data).' Choose by whole response vs. an individual data piece, and framework integration vs. manual.
- ISR is stale-while-revalidate, automatically gaining 31-day persistent cache, 300ms global purge, request coalescing (coalescing simultaneous access to the same uncached path into one function call per region), and instant-rollback resilience. It works on Next.js/SvelteKit/Nuxt/Astro.
- On-demand revalidation reflects instantly with revalidatePath/revalidateTag. In the Cache Components (PPR) era, use cacheTag and updateTag to invalidate per tag while having both a static shell and dynamic holes.
- Function-response CDN caching is layered control with three headers: Cache-Control (browser), CDN-Cache-Control (downstream CDN), and Vercel-CDN-Cache-Control (Vercel only). s-maxage and stale-while-revalidate are key. Vercel-CDN-Cache-Control is not returned to the browser.
- Always verify the result with the x-vercel-cache header (HIT/MISS/STALE/PRERENDER). Responses with Authorization/Set-Cookie, over 10MB, or private are not cached.

---

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](https://vercel.com/docs/incremental-static-regeneration), [CDN Cache](https://vercel.com/docs/edge-network/caching), and [Runtime Cache](https://vercel.com/docs/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](/blog/nextjs-16-app-router-cache-components-data-fetching); this article concentrates on **"how caching works as the Vercel platform."** For the big picture, see the [Vercel production-operations guide](/blog/vercel-production-platform-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](https://vercel.com/docs/incremental-static-regeneration)).

- **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.

```ts
// 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`.

```ts
// 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")`.

```ts
// 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`.

```tsx
// app/dashboard/page.tsx
import { Suspense } from "react";

export default function Dashboard() {
  return (
    <>
      <StaticHeader />               {/* 静的シェル：即配信 */}
      <Suspense fallback={<Skel />}>
        <UserStats />               {/* 動的な穴：ストリーム */}
      </Suspense>
    </>
  );
}
```

```tsx
// キャッシュ境界を宣言（コンポーネント/関数単位）
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](/blog/nextjs-16-app-router-cache-components-data-fetching). 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](https://vercel.com/docs/edge-network/caching)).

```ts
// 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 `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.

```ts
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.

```ts
// 高コストな計算を 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.**

```bash
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](/blog/vercel-cost-active-cpu-pricing-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](/blog/core-web-vitals-nextjs-inp-lcp-cls-optimization-guide))

---

## 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](/blog/vercel-deployments-cicd-rollback-rolling-releases-guide) for safely shipping what you built.

> This article is based on the [ISR](https://vercel.com/docs/incremental-static-regeneration) / [CDN Cache](https://vercel.com/docs/edge-network/caching) / [Runtime Cache](https://vercel.com/docs/runtime-cache) official documentation (as of June 2026). Specs are updated, so confirm the latest values in the official docs when adopting in production.
