How Claude's prompt cache works

From zero: what Claude Code sends on every message, what Anthropic keeps in its cache, what that costs, when it expires, and what keep-warm, /rewind, /branch, /fork, subagents, /model and /compact do to it. Then: how to build the same things in your own harness on the Anthropic API.

1. Words this page uses

Each word below means one thing on this whole page. Skim it now and come back when a word is unclear.

WordMeaning
requestOne call from Claude Code (or your own program) to Anthropic's Messages API. One message of yours can cause several requests, one per tool round.
blockOne piece of a request: the system prompt, a message of yours, a reply, a tool call, a tool result.
SYSThe first blocks of every Claude Code request: tool definitions, system prompt, CLAUDE.md. Drawn as one block on this page.
tool call / tool resultA reply in which Claude asks to run a tool ("run this Bash command") / what the tool returned, sent back in the next request.
tokenThe unit the API counts and bills, roughly ¾ of a word.
prefixThe first part of a request, from its first block up to some block.
cache entryThe stored, already-processed form of one prefix, kept on Anthropic's side for a while.
cache breakpoint 🚩A mark on a block of a request that says "store an entry ending after this block". In the API it is a cache_control field.
TTLTime to live: how long an entry survives after it was last used. 5 minutes or 1 hour.
cache read / cache write / uncached input / outputThe four kinds of tokens in every request; explained in rule 3.
sessionOne conversation in Claude Code: a list of messages it keeps on your machine. The API has no idea sessions exist (part 7).
side requestA request Claude Code makes outside your conversation: a subagent, a forked subagent, keep-warm's ping, compaction.
keep-warmA Claude Code mod that sends one tiny request after 50 idle minutes, so your cache entries don't expire (part 6).
forkThree different things share this word: /fork (a copy of your session that runs in the background), a forked subagent (/subtask), and keep-warm's $.model.fork (your last request sent again). This page always says which one.
checkpointOnly /rewind's snapshot of files and conversation position. Not related to cache breakpoints.

2. Why a cache exists at all

2.1 Claude remembers nothing between requests

Every API request stands alone: the model keeps no memory of the previous one. So to continue a conversation, Claude Code sends the whole conversation again, every time, with the new part added at the end.

Claude Codeon your machine
Anthropic APIruns the model

Each time, the request carries everything from before plus the newest part (white outline). In a long session that is hundreds of thousands of tokens per request.

2.2 What is inside one request

A request is a list of blocks, always in this order. The order matters (rule 1).

BlockWhat it holdsChanges how often
Tool definitions + system promptClaude Code's instructions and the description of every toolRarely: when the set of tools changes
Project contextCLAUDE.md, memory, rulesAt session start, /clear, /compact
Messages, oldest firstYour messages, Claude's replies, tool calls, tool resultsGrows on every request

2.3 Why that would be slow and expensive

Before the model can answer, it must process every input token. Without a cache, a 200,000-token conversation means processing 200,000 tokens on every message, although 99% of them are the same as last time. Prompt caching lets the API skip the part it processed recently, and bill that part at a tenth of the normal price or less.

3. The rules of the cache, one at a time

Rule 1. Entries are found by the exact start of the request

An entry is found again only when a later request starts with exactly the same blocks, in the same order, byte for byte, sent to the same model. One changed character in SYS, and nothing after it can match. That is why the blocks that change least come first.

Rule 2. A cache breakpoint 🚩 says where an entry ends

The request marks the blocks after which the API should store an entry (up to 4 marks per request). Claude Code places them for you: one at the end of the latest message on every request, and others earlier, such as after the system prompt. Claude Code's docs don't list where every 🚩 goes, so this page simplifies: it draws two per request, after SYS and after the last block.

After three messages, Claude Code has sent three requests. Each one put a 🚩 after SYS and a 🚩 at its end, so the cache holds these entries. Each is a separate, whole prefix:

entry 1:SYS🚩
entry 2:SYSmsg 1🚩
entry 3:SYSmsg 1reply 1msg 2🚩
entry 4:SYSmsg 1reply 1msg 2reply 2msg 3🚩

There is no entry "reply 1 alone" or "SYS … reply 1": no request ever put a 🚩 right after reply 1. A new request that starts SYS, msg 1, reply 1, msg 2, reply 2, msg 3, reply 3, msg 4 matches entry 4 (the longest one it starts with) and pays the cache-read price for those blocks. Entries 1 to 3 are not used separately; they are just shorter prefixes of the same blocks.

Entries exist only at breakpoints. A reply in the middle of a request is part of the entry that ends at the next 🚩, but there is no separate entry that ends right after that reply. Several scenarios in part 4 turn on this.

Rule 3. Every request is split into four kinds of tokens

When a request arrives, the API looks for the longest prefix that ends where a live entry ends (it searches back from each 🚩, up to 20 blocks). Then every token falls into one of these:

cache read: inside the matched entry. Not processed again. cache write: after the match, up to the last 🚩. Processed and stored. uncached input: after the last 🚩. Processed, not stored. output: what Claude generates. Not stored.
KindField in the API's usage reportPrice vs a normal input token
cache readcache_read_input_tokens0.1× on most models; 0.05× on Opus 5.5 and Sonnet 5.5; 0.025× on Fable 5.1
cache writecache_creation_input_tokens1.25× (5-minute entry) or 2× (1-hour entry)
uncached inputinput_tokens1×
outputoutput_tokensthe model's output price

On a Max plan you don't pay per token inside your plan's usage. We assume the same ratios decide how fast that usage runs out; Anthropic's docs don't say. inferred

Rule 4. A reply is not cached when it is written

The cache stores only what was sent in. A reply is output, so at first it exists only on your machine. It enters the cache on the next request, when Claude Code sends it back before that request's 🚩.

Rule 5. Entries expire; every read restarts the timer

An entry lives for its TTL after its last use. The timer starts when that request starts, not when its reply ends. Each request that reads through it restarts the timer, for free. If nothing reads it for the whole TTL, it is deleted, and the next request processes and writes everything again: the slow, expensive turn. Claude Code asks for 1 hour for your main conversation on a subscription (within its usage) and 5 minutes for side requests. With an API key, usage credits or a cloud provider, it asks for 5 minutes for both, unless you set promptCacheTtl. Below, one hour runs in 6 seconds: green ticks are requests reading the entry; then you stop.

0:00entry writtenTTL 60 min

4. Step through it: pick a scenario

Each card is one situation, written as the question it answers. Pick one, then press Next (or click the progress bar). Every step animates in order: the clock moves, the request is assembled, the API matches it against the cache, the usage comes out, the cache updates, and your conversations update.

What the 🚩 means in the panels. In panel 1, a 🚩 on a block means this request asks for an entry ending after that block. Panel 3 lists the entries that exist. Entries have no flags of their own: each row exists because some request put a 🚩 at that spot, and it ends exactly there. "written by step N" tells you which request it was.
cache readcache writeuncached inputoutput (not cached)no longer sent🚩 cache breakpoint
t = 0:00
1 · The request that is sent
2 · What the API reports back (usage)
3 · The cache on Anthropic's side
One row = one cache entry = one whole prefix: every block from the start of the request up to a 🚩 ("covers"). Blocks are not cached one by one. Rows overlap: a longer row repeats all the blocks of the shorter ones. A request can only use a row as a whole, and only if the request starts with exactly those blocks. The bar is the time the row has left.
4 · Your conversations, on your machine
What Claude Code will send next time, per session. Dashed = no longer sent.
What happened, and why

A model built from the rules above. Token sizes are made up but realistic. Block widths are not to scale. The 20-block search limit is left out; it never matters here. The cost line uses the standard prices: read 0.1×, write 2× for a 1-hour entry or 1.25× for a 5-minute one. On Opus 5.5 and Sonnet 5.5 a read costs 0.05×.

5. Reading the numbers Claude Code shows you

Every response reports the four counts from rule 3. The context-meter mod adds them up per message:

◆ ctx 7% · sent 214k in 3 requests (99% cached) · out 1.4k · $0.0270

PartMeaning
ctx 7%The size of the conversation now, as a share of the model's context window.
sent 214k in 3 requestsread + write + uncached, summed over this message's requests. Each request re-sent the whole ~71k conversation, so "sent" is far bigger than the conversation.
(99% cached)cache read ÷ sent.
out 1.4kOutput tokens.
$0.0270What the message cost at API prices.

A cache bust (counted at the right of the status line) is a request that read nothing. A subagent's first request usually counts as one: it starts its own conversation with a different SYS, so it can't read your entries (scenario "A normal subagent").

6. The keep-warm ping, in detail

After 50 idle minutes the keep-warm mod calls $.model.fork. Claude Code's mod API types describe it as: the main conversation's last request again (same model, system prompt, tools, messages) with one extra message after it, and "its own tail never cached". Scenario "Away 61 minutes, with keep-warm" animates it.

SYS20,000
message 1 · reply 1 · message 2🚩1,300
ping: "reply pong"15 · no new entry
→
"pong"discarded
  1. Everything up to message 2 is identical to your last request, so it is a cache read, and that read restarts the timers of the entries along that chain.
  2. The ping is processed but not stored.
  3. "pong" is output, never stored, and the mod discards it. Your conversation does not change.
  4. Your next real request starts exactly like the entry the ping refreshed, so it matches.
"Doesn't the fork move the breakpoint to after the ping?" No. A breakpoint is not a pointer owned by your session; every request carries its own. Even if the ping had stored an entry ("…message 2 + ping"), that would be an additional entry. Storing one never removes or changes another, and your session never sends the ping, so it would never match that extra one. Your session keeps matching the entry that ends at message 2.

Is "ping / pong" anything special?

No. "Reply pong" is just a short message I chose; any tiny message works. Nothing in the API knows about pings. What makes it work is ordinary prompt caching:

So yes, you can do the same in your own projects with the plain Messages API. Part 8 has the code. The ping is a side request with a 5-minute TTL, but that doesn't shorten your entry: while your entry is warm, the ping writes nothing, and in our test the session was still fully cached 11 minutes after the ping.

Keep-warm and idle compaction. Claude Code's own idle compaction is on by default: on the 1-hour TTL it compacts a conversation of 200,000 tokens or more after about 54 idle minutes. The docs don't say whether a keep-warm ping resets that idle clock, so on a big session it may compact anyway, a few minutes after the ping. To keep long idle sessions whole, set "idleCompaction": false in your settings (Claude Code 2.1.290 or later). docs not tested

7. Sessions, rewind, branch and fork, from the API's point of view

7.1 The API has no sessions

The Messages API takes a model, a system prompt, tools, a list of messages and a max-tokens limit, and returns one response. It keeps no conversation on its side. The only thing it keeps is the prompt cache, and the cache is keyed by content and model, not by session, user or branch (it is shared within one workspace on the Claude API, and within one organization on Amazon Bedrock and Google Cloud).

So every Claude Code feature below is the same move underneath: Claude Code decides which list of messages to send next. The cache simply rewards lists that start the same way as a recent one.

In Claude CodeWhat Claude Code does with the message listWhat the API seesCache effect
a sessionKeeps one list; appends each message, reply, tool call and resultRequests, each extending the previous oneEach reads the previous one's entry and writes the new tail
/rewind (restore conversation)Cuts the list back to before the chosen message; can also restore files from its local snapshotsA request that starts like an older one, then differsReads the longest live entry in the kept part
/branchCopies the list into a new session, switches you into it; the original stays on diskRequests that start exactly like the original'sThe copy's first request reads the original's entries; after that they diverge
/fork (agent view)Copies the list into a new background session and adds an instruction message at the end; you stay in the originalSame as branch, plus one message at the endReads the original's entries
/subtask (forked subagent)Sends your list + a brief as a separate run; keeps that run's tool calls out of your list; adds only its final resultA side request that starts like yoursReads your entries; its own tail gets a 5-minute entry
subagentStarts a new list with its own system prompt and toolsAn unrelated requestIts own entries, 5-minute TTL
keep-warm ($.model.fork)Re-sends the last request + a ping; discards the replyA request that starts like your last oneRefreshes your entries; writes nothing while they are warm
/compactSends the list + "summarize"; then replaces the list with the summaryOne request like yours, then requests that start with the summaryReads once (if warm); then a new, short chain
/modelSame list, different model fieldSame content, other modelSeparate cache per model

7.2 Branch vs fork vs subtask

/branch

You move into a copy. The original stays on disk, unchanged, and you return with /resume. Same process and folder. Use it to try another approach and keep the old path.

/fork

A copy runs as its own background session while you stay in the original. It gets an instruction message at the end, usually works in its own worktree, and the two are independent from then on.

/subtask

Not a session: a helper run that sees your whole conversation, works on the side, and sends back only its result as a message in your conversation.

At the API level all three are the same: a request whose messages start like yours. The differences are what Claude Code does around it: which list keeps growing, which session you are in, where files get edited, and what comes back.

7.3 Edge cases with fork, branch and rewind

CaseWhat happens
Rewind inside a branchOnly the branch's list is cut. The original's conversation, its checkpoints, and its cache entries are untouched. Both share entries for the part they have in common. Scenario "/branch, then rewind the branch".docs model
Rewind the original after branchingThe mirror image: the branch is untouched.docs model
"Restore code" in a branchRewind restores files on disk. /branch runs in the same folder as the original, so restoring code in one changes the files the other sees too. A /fork copy usually edits in its own worktree, so its files are separate.inferred
Rewind a branch to before the branch pointCheckpoints are saved with the conversation, and /branch copies the conversation, so the earlier prompts should be in the branch's rewind menu too.not tested
The same session resumed in two terminals, without branchingMessages from both interleave into one transcript. Use /branch or claude --continue --fork-session instead.docs
Branch after a long breakBoth are cold. Branching copies the list, not the cache; the first request of either pays the full write.model
Keep-warm in the original, branch laterThe ping keeps the original's chain warm, and the branch starts with that chain, so the branch's first request hits.model
A reply shared by both linesIt is written once per line, because no entry ends right after it (rule 2). Small, but it is why a branch's or rewind's first request writes a little more than just your new message.model

8. Build it yourself on the Anthropic API

Everything in part 7 can be built in your own harness, because none of it is a server feature. Sessions, rewind, branch, fork, subtasks, subagents, compaction and keep-warm are all "decide which messages to send". The cache does the rest, if you follow these rules:

Rule for your harnessWhy
Put a cache_control 🚩 on the last block of every request, and one at the end of the system prompt.Entries exist only where you put 🚩 (rule 2). Up to 4 per request. A top-level cache_control field (the API calls it automatic caching) puts the last one for you.
Never change anything before the end: same tools, same order, same system text, never edit old messages. Keep tool_choice, the thinking settings and effort the same. No timestamps or random IDs in the system prompt.One changed byte early on and nothing after it matches (rule 1).
Keep the same model for a conversation.Each model has its own cache.
Ask for "ttl": "1h" if users pause for more than 5 minutes.The default entry lives 5 minutes; a 1-hour entry costs 2× to write instead of 1.25×.
In long tool loops, add a 🚩 partway if a single request appends 20 or more blocks.The search for an earlier entry looks back only 20 blocks from each 🚩. A run of parallel tool calls, or of their results, counts as one.
Check usage.cache_read_input_tokens on every response.It is the only proof the cache worked.
Keep prompts above the model's minimum cacheable length.Very short prefixes are not cached at all, and the API returns no error. The minimum is per model: 512 tokens on current models such as Opus 5.5, up to 4,096 on some older ones.

A sketch in Python with the official SDK. Not run: it shows the shape, check names against the current API docs.

# pip install anthropic
import copy, anthropic

client = anthropic.Anthropic()
MODEL = "claude-opus-5-5"
TTL = {"type": "ephemeral", "ttl": "1h"}
SYSTEM = [{"type": "text", "text": open("system.md").read(), "cache_control": TTL}]   # 🚩 after SYS
TOOLS = [...]                       # same list, same order, every request

def with_flag(messages):
    """A copy of the list with a 🚩 on its very last block."""
    msgs = copy.deepcopy(messages)
    last = msgs[-1]
    if isinstance(last["content"], str):
        last["content"] = [{"type": "text", "text": last["content"]}]
    last["content"][-1]["cache_control"] = TTL
    return msgs

def send(session, text):
    """A normal turn: append, send everything, append the reply.
    No tool loop here: a tool_use reply needs tool_result blocks in the next message."""
    session.append({"role": "user", "content": text})
    r = client.messages.create(model=MODEL, max_tokens=4096, system=SYSTEM,
                               tools=TOOLS, messages=with_flag(session))
    session.append({"role": "assistant", "content": r.content})
    print(r.usage)                  # cache_read_input_tokens, cache_creation_input_tokens, input_tokens
    return r

def rewind(session, index):
    """/rewind: drop everything from this user message on. Pick an index of a real
    user prompt, not a tool result, so roles stay valid."""
    del session[index:]

def branch(session):
    """/branch or /fork: an independent copy. Its first request reads the original's cache."""
    return copy.deepcopy(session)

def subtask(session, brief):
    """/subtask: a side run on a copy; only its answer comes back.
    Put the answer into your next message in the main session."""
    side = branch(session)
    return send(side, brief).content[-1].text

def keep_warm(session):
    """Keep-warm: your last request again, with max_tokens=0, so no reply. Nothing is kept."""
    msgs = with_flag(session[:-1])  # the last request exactly: everything before the last reply
    r = client.messages.create(model=MODEL, max_tokens=0, system=SYSTEM,
                               tools=TOOLS, messages=msgs)
    return r.usage.cache_read_input_tokens   # about the whole conversation if it worked

def compact(session):
    """/compact: ask for a summary over the cached list, then start a new list from it."""
    side = branch(session)
    summary = send(side, "Summarize this conversation so work can continue.").content[-1].text
    session[:] = [{"role": "user", "content": "Summary of our work so far:\n" + summary},
                  {"role": "assistant", "content": "Understood."}]

On the plain API you need no ping. max_tokens=0 is the API's documented pre-warming mode: it reads the prompt into the cache and returns no output, so no output tokens are billed. It is refused with streaming, extended thinking, structured outputs or a forced tool_choice. Claude Code's $.model.fork has no such option, which is why keep-warm sends a ping. Run keep_warm on a timer a few minutes before the TTL runs out, and stop after a few rounds if nobody comes back.

9. What resets the cache, and what doesn't

Anything that changes an early block changes the start of every later request. From Claude Code's prompt-caching docs:

The next request is read from scratch after…The cache is kept after…
switching model (/model)
changing effort, on most models (not on Opus 5.5 / Sonnet 5.5 / Haiku 5.5 / Fable 5.1 with a subscription or API key)
turning on fast mode (once per conversation)
compaction (/compact, auto or idle compaction)
adding or removing tools, when tools are loaded up front (not the default)
many images, when old ones get dropped
a new Claude Code version (in a new session)
editing files
editing CLAUDE.md mid-session (it applies only after /clear, /compact or a restart)
MCP servers connecting or leaving, with tool search on (the default)
changing permission mode or output style
running skills and slash commands, /recap
spawning a subagent
/rewind (restore conversation or code; its two "Summarize" options replace part of the conversation, so they work like /compact), /fork
/branch (not on that page: it copies the conversation, per the sessions docs)

10. What we measured

In the browser terminal on 2026-10-14, with Claude Haiku 5.5 sessions:

TestResult
One keep-warm ping in a 69k-token session68.8k cache read, about 400 uncached (the ping), 4 output tokens.
The next real message after that ping99% cache read. Asked whether the conversation contained a ping, Claude said no.
Two identical 128k sessions, idle 61 minutes, then the same promptWithout a ping: 24% cache read, $0.0994.
With a ping at minute 50: 100% cache read, $0.0065, about 15× cheaper.

The session without a ping still got 24%: most likely its SYS stayed warm because the other test sessions in the same folder sent the same SYS (Claude Code's docs say sessions run in parallel in the same directory read each other's cache, and sequential ones do when their startup git status matches). Not tested.

11. Where each fact comes from

FactSource
Requests re-send everything; prefix matching; block order; 1 h main / 5 min side on a subscription, 5 min for both on an API key; reads restart the timer; what resets the cache; rewind keeps entries warm; restoring files doesn't touch the cache; /fork adds its instruction at the end so the cache stays intact; a forked subagent reads the parent's cachedocs Claude Code: prompt caching
/branch copies the conversation and switches you into it; two terminals on one session interleavedocs Claude Code: sessions
/fork copies into a background session; the two are independentdocs Claude Code: agent view
/subtask inherits system prompt, tools, model and history; only its result comes backdocs Claude Code: subagents
Rewind checkpoints and actionsdocs Claude Code: checkpointing
cache_control, up to 4 breakpoints, the ~20-block search, the usage fields, the price multipliers, the 1-hour TTL, the minimum length, the timer starting with the request, workspace and organization scope, max_tokens=0 pre-warmingdocs Anthropic API: prompt caching
Keep-warm's fork repeats the last request; its tail is never cacheddocs this Claude Code build's API types ($.model.fork, 2.1.299)
The ping read, the 99% follow-up, the 15× result, the session still fully cached 11 minutes after a pingmeasured our tests, 2026-10-14
Where exactly Claude Code puts its breakpoints; the 1-hour TTL of a /fork copy; all scenario numbers; the edge cases tagged model, inferred or not tested; the Python sketch; the 24% explanationmodel follows from the rules above; not measured