Placing cache_control Breakpoints on System Prompts and Tools
A cache_control breakpoint only caches what comes before it, in the exact order Claude assembles the request: tools first, then the system prompt, then the messages array.
Get that ordering wrong and breakpoints either cache too little to matter, or they get invalidated by content that should never have been in the cached prefix.
This page walks through where to put breakpoints, why the order matters, and how to layer more than one for finer-grained caching.
Summary
Every Claude API request has three logical sections that participate in the cached prefix, always in the same order: tools, then system, then messages.
A breakpoint marks the end of the content you want cached, so a breakpoint on the last tool caches only tools, while a breakpoint on the system prompt caches tools plus system together.
Content placed after the final breakpoint - almost always the tail of messages - is never cached and is reprocessed on every call.
Getting this right means putting your most stable content first and your most volatile content last, then placing breakpoints at the natural seams between them.
This page shows the request shape, the ordering rules, and worked examples with one breakpoint and with two layered breakpoints.
Recipe
Quick-reference recipe card - copy-paste ready.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=500,
tools=[
{
"name": "search_docs",
"description": "Search the internal knowledge base for a topic.",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
"cache_control": {"type": "ephemeral"},
}
],
system=[
{
"type": "text",
"text": "You are Acme Corp's support assistant. Follow the tone guide below.\n" + ("...tone guide text..." * 100),
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": "What's your refund window?"}],
)When to reach for this:
- Your request includes a system prompt or tool catalog that stays the same across many calls.
- You want to cache tools and system independently, because one changes more often than the other.
- You're seeing unexpectedly low
cache_read_input_tokensand suspect breakpoint placement, not content drift. - You're combining tool use with a long instruction set and want to control exactly what's eligible to cache.
Working Example
import anthropic
client = anthropic.Anthropic()
REFERENCE_DOCS = "Product catalog and return policy details...\n" * 400
TOOL_DESCRIPTION = "Look up a customer's order by order ID." * 15
def build_request(user_question: str) -> dict:
"""Build a request with two independent cache breakpoints:
one after the tool catalog, one after the system prompt.
"""
return {
"model": "claude-sonnet-5",
"max_tokens": 500,
"tools": [
{
"name": "lookup_order",
"description": TOOL_DESCRIPTION,
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
# Breakpoint 1: caches the tool catalog on its own.
"cache_control": {"type": "ephemeral"},
}
],
"system": [
{
"type": "text",
"text": (
"You are Acme Corp's support assistant.\n"
"Reference material:\n" + REFERENCE_DOCS
),
# Breakpoint 2: caches tools + system together.
"cache_control": {"type": "ephemeral"},
}
],
# Everything here is per-request and never cached.
"messages": [{"role": "user", "content": user_question}],
}
client = anthropic.Anthropic()
r1 = client.messages.create(**build_request("What's your refund window?"))
print("call 1:", r1.usage.cache_creation_input_tokens, r1.usage.cache_read_input_tokens)
r2 = client.messages.create(**build_request("Can I track order #4471?"))
print("call 2:", r2.usage.cache_creation_input_tokens, r2.usage.cache_read_input_tokens)What this demonstrates:
- Two breakpoints in one request: one right after the tool list, one right after the system prompt.
tools,system, andmessagesare built by a single function so the cached content is guaranteed byte-identical across calls.- Only the final user question changes between
r1andr2, and it correctly sits after both breakpoints, inmessages. - The second call shows
cache_read_input_tokensnonzero because the tools+system prefix matched exactly.
Deep Dive
How It Works
- Claude assembles the effective prefix in a fixed order:
tools, thensystem, thenmessages, regardless of the order the keyword arguments appear in your Python call. - A
cache_controlblock placed on the last element oftoolscaches everything intoolsup to and including that element. - A
cache_controlblock placed on asystemcontent block cachestools(if cached) plussystemup to that point, as one combined segment. - You can mark more than one breakpoint in a single request; each one closes off a segment, letting you get partial cache credit even when only the later segment changed.
- Anything after the last breakpoint - virtually always the tail of
messages- is reprocessed every time and never contributes tocache_read_input_tokens.
Where Breakpoints Commonly Go
| Breakpoint location | Caches | Use when |
|---|---|---|
Last tool in tools | Tool schemas only | Tool catalog is large/stable but system prompt changes per user or session |
End of system text block | Tools (if marked) + system prompt | System prompt is large and stable; typical default choice |
| Not used | Nothing | Prompt is short, or every part changes every call |
Python Notes
# system can be a plain string OR a list of content blocks.
# cache_control is only settable when system is the LIST form.
# Will NOT cache - system is a bare string, no cache_control possible:
system = "You are a helpful assistant."
# WILL cache - system is a list of one content block carrying cache_control:
system = [
{"type": "text", "text": "You are a helpful assistant.", "cache_control": {"type": "ephemeral"}}
]Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
cache_control | dict | Marks the end of a cacheable segment; {"type": "ephemeral"} or {"type": "ephemeral", "ttl": "1h"}. |
tools[i].cache_control | dict | Placed on a tool definition; caches that tool and everything before it in the list. |
system[i].cache_control | dict | Placed on a system content block; caches tools (if marked) plus system up to that block. |
Gotchas
- Putting
cache_controlon a bare stringsystem. A plain stringsystem="..."has no place to attachcache_control. Fix: always use the list-of-content-blocks form ofsystemwhen you want caching. - Assuming argument order in Python controls the cached order. Writing
messages=beforetools=in your function call has no effect on the request's logical order. Fix: remember Claude always evaluatestools->system->messages, regardless of keyword order in your code. - Putting user-specific text inside the cached system block. Interpolating a username or session ID into the system prompt text before the breakpoint invalidates the cache on every single request. Fix: keep the system block static; put anything per-user or per-session into
messages, after the breakpoint. - Marking every tool individually instead of just the last one.
cache_controlonly needs to go on the final tool in the list - it caches everything from the start oftoolsthrough that entry. Fix: mark one breakpoint per segment you want, not one per item. - Rebuilding the tool schema dict from scratch on every call. Even semantically identical Python dicts can serialize differently if key insertion order varies. Fix: build tool and system content from a single shared constant or function, not inline literals scattered across the codebase.
- Forgetting that a changed tool schema invalidates a later system breakpoint too. Because tools come before system in the prefix, a tool schema change breaks the system-level cache even if the system text itself never changed. Fix: treat tool schema changes as cache-affecting events, not just prompt-text changes.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Single breakpoint at end of system prompt | Simple prompts, one stable block | Tools and system prompt change independently and often |
| Two breakpoints (tools + system) | Tools and system have different change frequencies | The extra bookkeeping isn't worth it for a short-lived script |
| No cache_control at all | Prompt is short or already cheap | The prompt or tool catalog is large and reused across many calls |
FAQs
In what order does Claude actually build the cached prefix?
Tools first, then the system prompt, then the messages array - always in that order, independent of how you order keyword arguments in Python.
Can I put cache_control directly on a plain string system prompt?
No. system must be the list-of-content-blocks form for cache_control to have anywhere to attach. A bare string system prompt cannot be marked as cacheable.
Do I need a breakpoint on every tool in my tools list?
No, one breakpoint on the last tool you want included is enough - it covers everything in the list up to and including that entry.
What happens to content placed after the last breakpoint?
It's processed fresh on every request and never contributes to a cache hit. This is almost always the current turn's content in messages, which is exactly where per-request content belongs.
Why would I use two breakpoints instead of one?
To get independent caching for content that changes at different rates - e.g. a stable tool catalog and a system prompt that's tweaked more often. Two breakpoints let a change to one still hit cache on the other.
Does a tool schema change affect the system prompt's cache?
Yes, because tools come before the system prompt in the prefix. If your tool schema changes, the segment covering "tools + system" is invalidated even if the system text itself is unchanged.
Is it safe to put a per-user greeting inside the cached system block?
No - anything that varies per user or per request should go in messages, after the breakpoint. Putting it in the cached system block invalidates the cache on every call.
Does the order of keyword arguments in my Python call change caching behavior?
No. Whether you write tools=..., system=..., messages=... or a different argument order in your function call, Claude still evaluates the request in the fixed tools -> system -> messages order.
How many breakpoints can I use in one request?
More than two is technically possible but rarely useful in practice; two (tools, then system) covers the vast majority of real caching needs without added complexity.
What's the most common mistake that breaks breakpoint caching?
Rebuilding cached content (system text, tool schemas) as inline literals scattered through the codebase instead of from a single shared constant, which lets small, accidental differences creep in between calls.
Related
- How Prompt Caching's Prefix Match Actually Works - the underlying exact-match mechanism this ordering depends on.
- Prompt Caching Basics - a first cache_control breakpoint end to end.
- Debugging Silent Cache Invalidators in Long Prompts - find what's breaking a breakpoint you've already placed.
- Defining Tool Schemas with name, description, and input_schema - how tool schemas are structured before you cache them.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.