---
title: "Cache Control"
description: "Use provider-aware cache control to reduce repeated input costs when your stack supports it."
canonical_url: "https://vercel.com/academy/build-ai-agent-harness/cache-control"
md_url: "https://vercel.com/academy/build-ai-agent-harness/cache-control.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-10-30T14:12:59.894Z"
content_type: "lesson"
course: "build-ai-agent-harness"
course_title: "Build Your Own AI Coding Agent Harness"
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>

# Cache Control

# Cache Control

Pruning solves one half of the problem. The other half is that every call still sends the parts of the context that didn't change.

Your system prompt is the same on call ten as it was on call one. The early user prompt is the same. The tool definitions are the same. The provider has seen these tokens before, and you're paying to send them again.

Cache control is the provider's way to say "I remember this, don't process it again." Where it's supported, the cost drop is real. Where it isn't, the pattern still teaches a useful habit: separate stable context from fresh context.

## Outcome

A `prepareStep` pipeline that prunes old tool results, then marks cache breakpoints on what's left. On providers that support cache control, repeated input costs drop substantially.

## Fast Track

1. Add an `addCacheControl(messages)` helper that sets cache breakpoints
2. Compose it after `pruneMessages` in `prepareStep`
3. Mark the first message and the last message, and leave the middle alone

## Hands-on Exercise 5.4

Wire cache control behind the pruning step.

**Requirements:**

1. Write `addCacheControl(messages)` that returns a new array of messages with `providerOptions.anthropic.cacheControl` set where appropriate
2. The first message (the initial user prompt) always gets a breakpoint
3. The last message always gets a breakpoint
4. Every message in between stays unmarked
5. Update `prepareStep` to call `addCacheControl(pruneMessages(...))`

**Implementation hints:**

- `cacheControl: { type: "ephemeral" }` is the Anthropic-flavored shape. Other providers use different keys. If you're hitting one that doesn't support cache control, the call still works, the header just gets ignored
- The cache breakpoints work by prefix. Marking message 5 as cacheable caches everything up to and including message 5. The provider checks the prefix on the next call
- In an agent loop, new messages get appended to the end. Nothing gets swapped out, so the last message is the right place for a breakpoint: the next step's prefix starts with everything you just sent
- Anthropic caps how many breakpoints one request can carry ([prompt caching docs](https://platform.claude.com/docs/en/docs/build-with-claude/prompt-caching)). Mark a couple of messages, not every message

### The helper

```ts title="src/cache.ts"
import type { ModelMessage } from "ai";

export function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
  return messages.map((msg, i) => {
    if (i === 0 || i === messages.length - 1) {
      return {
        ...msg,
        providerOptions: {
          ...msg.providerOptions,
          anthropic: { cacheControl: { type: "ephemeral" } },
        },
      };
    }
    return msg;
  });
}
```

Two messages get the marker. The last one is where Anthropic recommends a breakpoint for a growing conversation. Each step appends to the end, so the next request starts with the exact prefix this one just wrote, and the provider reads it back from cache.

The first one is our insurance policy against pruning. `pruneMessages` drops old tool results from the middle of the history, and when the middle changes, the prefix behind the last breakpoint changes with it. The breakpoint on the first message caches the part pruning never touches: the tool definitions, the instructions, and the original prompt, which all come before it.

You might be tempted to mark every older message instead. Don't. Anthropic limits the number of breakpoints per request, and a long session blows past that limit fast.

### Compose with pruning

In `index.ts`, the `prepareStep` from 5.2 becomes a pipeline:

```ts title="index.ts"
import { addCacheControl } from "./src/cache";

prepareStep: ({ messages }) => ({
  messages: addCacheControl(
    pruneMessages({ messages, toolCalls: "before-last-3-messages" }),
  ),
}),
```

Pruning happens first because it changes how many messages there are. Caching happens second on whatever messages survive.

### What the savings look like

The numbers depend on the provider, the cache hit rate, and how much of your context is stable. For a typical Anthropic-backed agent running long sessions:

| Component                 | Cost without cache    | Cost with cache      |
| ------------------------- | --------------------- | -------------------- |
| 50 calls, 200K input each | Full input every call | Stable prefix cached |
| 10M tokens at full price  | \~$30 per session     | \~$6 per session     |

That's an order-of-magnitude swing on long sessions. On short sessions, the difference is smaller because the cache doesn't have time to amortize. The discipline still applies.

\*\*Note: Cache control is provider-specific\*\*

Anthropic uses `cacheControl` on message parts. OpenAI uses different headers and a different model. Some providers don't expose caching at the prompt level at all. The pattern (separate stable from fresh) survives those differences. The exact `providerOptions` shape doesn't.

### Why the pattern matters even where caching isn't supported

You're forcing yourself to think about which parts of the prompt are stable. That's a useful habit. It tells you whether your system prompt is doing too much work per-call (rebuilt every time) or just right (built once, reused).

If caching disappeared tomorrow, the discipline of identifying stable context would still be worth keeping. Stable context is also easier to test, easier to version, and easier to reason about than context that changes shape every call.

## Try It

Run a multi-step task and watch the token counts:

```bash title="Terminal"
bun run index.ts . "Read package.json, tsconfig, index.ts, then summarize"
```

If your provider returns cache hit info in the usage object, log it:

```ts title="index.ts (temporary)"
onStepEnd: ({ usage, stepNumber }) => {
  console.error(
    `Step ${stepNumber}: ${usage.inputTokens} input, ${usage.outputTokens} output, ${usage.inputTokenDetails.cacheReadTokens ?? 0} cached`,
  );
},
```

Step 0 writes the cache, so its cached count is `0`. From step 1 onward you should see `inputTokenDetails.cacheReadTokens` above zero, covering the stable prefix the earlier steps wrote.

\*\*Note: Seeing 0 cached tokens on every step?\*\*

This is expected on short runs. Anthropic only caches a prefix above a minimum token count, and the minimum depends on the model ([current minimums](https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#limitations)). A small project with a short prompt can sit under it, and the provider processes the request without caching even though your markers are correct. Point the agent at a bigger repo, or log `inputTokenDetails.cacheWriteTokens` too, to see whether anything got written.

```bash title="Terminal"
npx tsc --noEmit
```

## Commit

```bash
git add src/cache.ts index.ts
git commit -m "feat(context): add cache control to stable messages"
```

## Done-When

- [ ] `addCacheControl(messages)` marks the first and last messages, and nothing else
- [ ] `prepareStep` runs pruning then caching, in that order
- [ ] On a provider that supports caching, `inputTokenDetails.cacheReadTokens` shows up in usage
- [ ] `npx tsc --noEmit` passes

\*\*Note: Build a token-budget dashboard\*\*

You've got telemetry from lesson 5.1, pruning from 5.2, caps from 5.3, and caching from this lesson. Wire them together. After each step, log: input tokens (uncached), cached tokens, output tokens, and the running cost. At session end, print the total cost and what the session would have cost without pruning and caching. The number is what makes the discipline worth doing.

## Solution

```ts title="src/cache.ts"
import type { ModelMessage } from "ai";

export function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
  return messages.map((msg, i) => {
    if (i === 0 || i === messages.length - 1) {
      return {
        ...msg,
        providerOptions: {
          ...msg.providerOptions,
          anthropic: { cacheControl: { type: "ephemeral" } },
        },
      };
    }
    return msg;
  });
}
```

```ts title="index.ts (prepareStep)"
prepareStep: ({ messages }) => ({
  messages: addCacheControl(
    pruneMessages({ messages, toolCalls: "before-last-3-messages" }),
  ),
}),
```


---

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