---
title: "Runtime Cache"
description: "Wrap Saturday's stock-check warehouse call with Next.js 16's 'use cache: remote' directive so repeat lookups for the same SKU and size hit the Runtime Cache instead of invoking the function path that talks to the warehouse."
canonical_url: "https://vercel.com/academy/optimize-your-vercel-account/runtime-cache"
md_url: "https://vercel.com/academy/optimize-your-vercel-account/runtime-cache.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-10-28T18:12:53.734Z"
content_type: "lesson"
course: "optimize-your-vercel-account"
course_title: "Optimize Your Vercel Account"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Runtime Cache

# Runtime Cache

It's drop day. Ten thousand sneakerheads have your stock-check endpoint queued in browser tabs, fingers on the refresh button. The Foundry just went live in size 10 with 47 pairs available. Your function runs 10,000 times, calls the warehouse 10,000 times, and the warehouse says the same thing 10,000 times: yes, 47 of size 10. Then 46. Then 45.

You don't need to ask the warehouse 10,000 times for the same answer. You need to ask it once and remember the result for a few seconds. That's what the Runtime Cache does. Wrap the slow call in `'use cache: remote'`, give the cached result a short revalidation window, and the next thousand requests for the same SKU and size answer themselves from a regional cache shared across server instances in the same region instead of invoking the warehouse path.

The function still runs. It just stops doing the expensive thing on every call. Cache reads and writes are metered too, so check [Runtime Cache pricing](https://vercel.com/docs/caching/runtime-cache) before you wrap every function in sight.

\*\*Note: What is the Runtime Cache?\*\*

A remote cache for the results of server-side work. In Next.js 16 you
reach it with the `'use cache: remote'` directive: the function's return
value is stored keyed by its arguments, so server instances can share the
stored answer instead of repeating the work.

## Outcome

`fetchStockFromWarehouse` in `app/api/stock-check/route.ts` is wrapped with `'use cache: remote'` so repeat requests for the same SKU and size are served from the Runtime Cache.

## Hands-on exercise 3.2

Four file changes. Remove the legacy route-segment revalidation setting, enable Cache Components, move the home page's date-dependent banner out of the prerender, and add the remote cache directive to the function.

**Requirements:**

1. Open `app/drops/page.tsx` and delete `export const revalidate = 60`. Route Segment Config options such as `revalidate` are disabled when Cache Components is enabled; Lesson 3.3 replaces this timer with tag-based revalidation.
2. Open `next.config.ts` and add `cacheComponents: true` at the top of the config object. The `'use cache: remote'` directive requires this flag in Next.js 16.
3. Open `app/page.tsx`. The NEXT DROP banner calls `getNextDrop()`, which reads today's date, and Cache Components won't prerender a page that does. Move the banner into an async `NextDrop` component that starts with `await connection()` (imported from `next/server`), and render it inside `<Suspense>`.
4. Open `app/api/stock-check/route.ts`
5. Import `cacheTag` and `cacheLife` from `next/cache`
6. Add `'use cache: remote'` as the first line inside `fetchStockFromWarehouse`
7. Tag the cached result with ``cacheTag(`stock-${sku}`)`` so we can invalidate by SKU later
8. Set the cache lifetime with `cacheLife({ revalidate: 60, expire: 300 })` (refresh after a minute, hard expire after five)
9. Commit, push, redeploy

**Implementation hints:**

- The `'use cache: remote'` directive is function-scoped. Putting it at the top of `fetchStockFromWarehouse` caches that function's results keyed by its arguments in shared storage. Use plain `'use cache'` for in-memory or static-shell work; the remote directive is the fit here because multiple server instances need to shield the same slow backend.
- `connection()` tells Next.js the banner depends on the incoming request, so it renders at request time while the rest of the home page stays prerendered. The banner is the only part of the page that changes from day to day, so it's the only part that waits.
- Enabling Cache Components disables legacy Route Segment Config options. Removing the drops page's `revalidate` export before you deploy avoids mixing the old and new caching models; Lesson 3.3 finishes the replacement with a tag.
- `revalidate: 60` doesn't mean the cache is purged every 60 seconds. It means: on the first request after 60 seconds, serve the stale value and refresh in the background. The user sees an instant response.
- `expire: 300` is the hard ceiling. After 5 minutes the cached value is gone and the next request waits for a fresh warehouse call.
- The tag (`stock-${sku}`) is your invalidation handle. From anywhere in the app you can call `revalidateTag('stock-sat-foundry-onyx')` to dump the cached entry for that one SKU. That's how a CMS webhook would tell the site about new inventory.

## Try It

After the redeploy lands, send the same stock-check request twice in a row:

```bash
time curl -s -X POST https://YOUR_DOMAIN/api/stock-check \
  -H "Content-Type: application/json" \
  -d '{"sku":"sat-foundry-onyx","size":10}'

time curl -s -X POST https://YOUR_DOMAIN/api/stock-check \
  -H "Content-Type: application/json" \
  -d '{"sku":"sat-foundry-onyx","size":10}'
```

The first call should look like this:

```
{ "available": true, "count": 5 }
real    0m0.847s
```

The second call should look like this:

```
{ "available": true, "count": 5 }
real    0m0.082s
```

The first request fills the cache. The second hits the cache and skips the 800ms warehouse simulation. Same response body. Tenth of the time.

Now try a different size:

```bash
time curl -s -X POST https://YOUR_DOMAIN/api/stock-check \
  -H "Content-Type: application/json" \
  -d '{"sku":"sat-foundry-onyx","size":11}'
```

That's a fresh argument tuple, so it's a fresh cache key, so it's a slow first call:

```
{ "available": true, "count": 11 }
real    0m0.831s
```

The size-10 cache stays warm. The size-11 cache is now also warm. This is how caching by arguments works in practice.

## Done-When

- [ ] `next.config.ts` has `cacheComponents: true`
- [ ] `app/drops/page.tsx` no longer exports `revalidate`
- [ ] The home page's NEXT DROP banner renders from a `NextDrop` component inside `<Suspense>`, and the deploy builds
- [ ] `app/api/stock-check/route.ts` imports `cacheTag` and `cacheLife`
- [ ] `fetchStockFromWarehouse` begins with `'use cache: remote'`
- [ ] Second curl request to the same SKU/size returns in under 200ms
- [ ] Observability > Functions shows fewer invocations of `/api/stock-check` running the slow path during repeat traffic

## Troubleshooting

**The build fails with "Next.js encountered the unstable value `new Date()` while prerendering."**

This is expected if Cache Components is on and the home page still calls `getNextDrop()` at the top of `HomePage`. Next.js tries to prerender the whole page, finds a value that depends on when the request happens, and stops rather than freeze yesterday's date into the HTML. Move the banner into the `NextDrop` component from step 3 and the build goes through.

**TypeScript complains that `'use cache: remote'` isn't a valid directive.**

You're missing the `cacheComponents: true` flag in `next.config.ts`, or your Next.js version is below 16. Check `package.json` and confirm `next` is `^16.0.0`. If it's older, upgrade. If the build points at `export const revalidate`, remove that legacy export from `app/drops/page.tsx` as required above.

**The second request still takes 800ms.**

A few possibilities. The deployment might not have rebuilt yet, check the deployment timestamp. The cache might be partitioned by region, and your second request might have hit a different one, try several requests in a row from the same location. Or your tag and lifetime might not have applied because of a syntax error above them; check the build logs for warnings.

**The cache returns stale data for too long.**

The combination of `revalidate: 60` and `expire: 300` means stale data can be served for up to 5 minutes. If that's too long for inventory, lower `expire`. If you want truly fresh data on every request, you don't want caching for this route at all, and that's a fair call for some endpoints. The win here only exists because stock counts changing every 60 seconds is acceptable for a small indie brand.

## Solution

The finished `app/api/stock-check/route.ts` is in the [`complete` folder](https://github.com/vercel/academy-starters/blob/main/optimize-your-vercel-account/complete/app/api/stock-check/route.ts) if you want to check your version against it.

The updated function:

```ts title="app/api/stock-check/route.ts"
import { NextResponse } from "next/server";
import { cacheTag, cacheLife } from "next/cache";
import { getProductById } from "@/app/data/products";
import type {
  StockCheckRequest,
  StockCheckResponse,
} from "@/app/lib/types";

async function fetchStockFromWarehouse(
  sku: string,
  size: number
): Promise<StockCheckResponse> {
  "use cache: remote";
  cacheTag(`stock-${sku}`);
  cacheLife({ revalidate: 60, expire: 300 });

  await new Promise((resolve) => setTimeout(resolve, 800));

  const product = getProductById(sku);
  if (!product || !product.sizes.includes(size)) {
    return { available: false, count: 0 };
  }

  const deterministicCount = ((sku.length * size * 7) % 12) + 1;
  return { available: true, count: deterministicCount };
}

export async function POST(request: Request) {
  const body = (await request.json()) as Partial<StockCheckRequest>;
  if (!body.sku || typeof body.size !== "number") {
    return NextResponse.json(
      { error: "sku and size are required" },
      { status: 400 }
    );
  }
  const result = await fetchStockFromWarehouse(body.sku, body.size);
  return NextResponse.json(result);
}
```

And the config flag:

```ts title="next.config.ts"
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
```

The home page banner, moved behind `<Suspense>` (the finished [`app/page.tsx`](https://github.com/vercel/academy-starters/blob/main/optimize-your-vercel-account/complete/app/page.tsx) has the full markup):

```tsx title="app/page.tsx"
import { Suspense } from "react";
import { connection } from "next/server";
import { getNextDrop } from "@/app/data/drops";
import { getProductBySlug } from "@/app/data/products";
// ...other imports unchanged

export default function HomePage() {
  // ...liveProducts unchanged
  return (
    <div>
      {/* ...hero section unchanged */}
      <Suspense fallback={null}>
        <NextDrop />
      </Suspense>
      {/* ...IN STOCK section unchanged */}
    </div>
  );
}

// The next drop depends on today's date, so it renders at request time instead of at build time.
async function NextDrop() {
  await connection();
  const nextDrop = getNextDrop();
  const nextProduct = nextDrop ? getProductBySlug(nextDrop.productSlug) : null;
  if (!nextProduct || !nextDrop) return null;

  return (
    <section className="border-y border-line bg-white">
      {/* ...the NEXT DROP markup that used to sit in HomePage */}
    </section>
  );
}
```

Also remove `export const revalidate = 60` from `app/drops/page.tsx` before enabling Cache Components. The next lesson adds the replacement cache tag and webhook invalidation.

The cache shields the warehouse, the firewall shields the function. Together they handle Saturday's drop-day traffic shape, many requests, few unique answers.

Up next: the drops page itself is on a timer. Let's fix that.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
