Originally published at
[kalyna.pro]
If your app sends the same large system prompt, tool definitions, or document context on every request, you're paying full price to re-process those tokens every single time. Prompt caching lets Claude reuse the processed representation of a prompt prefix across requests β cache hits cost 90% less than normal input tokens. This guide covers how caching actually works, the pricing math for writes vs reads, where to place cache breakpoints, and worked cost examples for RAG apps and agents.
pip install anthropic
See the Claude API Tutorial if you're new to the Messages API, and Claude API Pricing Explained for base per-model pricing.
Caching works on prefixes. A request is processed top to bottom β system prompt, then tool definitions, then messages. You mark a cache breakpoint by adding "cache_control": {"type": "ephemeral"}
to a content block. Everything from the start of the prompt up to and including that block is cached as a unit.
On the next request, if the prefix up to a breakpoint is byte-for-byte identical to a cached prefix, Claude reads the cached version instead of reprocessing it β you only pay full price for whatever comes after the last matching breakpoint. If anything before a breakpoint changes, that cache entry misses and gets rewritten.
Using Claude Sonnet 4.6 ($3 / $15 per 1M tokens, input/output) as an example:
The same multipliers apply to every model's base price β for Haiku 4.5 ($0.25 / $1.25 per 1M) a cache read costs $0.025 / 1M; for Opus 4.7 ($15 / $75 per 1M) it costs $1.50 / 1M.
A RAG chatbot sends a 10,000-token retrieved-context block as the cached prefix, handling 100 requests/day at a steady pace.
Without caching: 100 Γ 10,000 Γ $3.00/1M = $3.00/day
With caching: first request writes the cache (10,000 Γ $3.75/1M = $0.0375). The remaining 99 read from cache (99 Γ 10,000 Γ $0.30/1M = $0.297). Total: β $0.33/day β about an 89% reduction, compounding with every extra request.
from anthropic import Anthropic
client = Anthropic()
LARGE_SYSTEM_PROMPT = open("knowledge_base.md").read() # 8,000+ tokens
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": LARGE_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": "Summarize the refund policy."}],
)
print(response.usage)
The first call writes the cache. Send the same system prompt again within 5 minutes, and cache_read_input_tokens=8000
, cache_creation_input_tokens=0
β those tokens now cost 90% less.
Tool schemas count as input tokens on every call, including every step of a tool-use loop. Cache them by adding cache_control
to the last tool in the list:
tools = [
{"name": "get_stock_price", "description": "...", "input_schema": {...}},
{"name": "get_exchange_rate", "description": "...", "input_schema": {...}},
{
"name": "get_current_time",
"description": "Get the current date and time in UTC.",
"input_schema": {"type": "object", "properties": {}},
"cache_control": {"type": "ephemeral"},
},
]
This caches all tool definitions as one prefix block β high leverage for agents, since tools
is resent unchanged on every loop step.
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": f"<document>{long_document}</document>",
"cache_control": {"type": "ephemeral"},
},
{
"type": "text",
"text": "What are the payment terms in section 4?",
},
],
}
],
)
Follow-up questions about the same document, sent within 5 minutes, only pay full price for the new question.
{
"type": "text",
"text": LARGE_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral", "ttl": "1h"},
}
The 1-hour cache costs 2Γ base to write (vs 1.25Γ for 5-minute), reads are still 0.1Γ. It pays off once a cached prefix is reused at least twice within the hour. For traffic denser than every 5 minutes, the default TTL is usually cheaper.
Up to 4 breakpoints per request:
system=[
{
"type": "text",
"text": STATIC_INSTRUCTIONS, # rarely changes β shared across all users
"cache_control": {"type": "ephemeral"},
},
{
"type": "text",
"text": USER_PROFILE_CONTEXT, # changes per user
"cache_control": {"type": "ephemeral"},
},
]
Claude checks for the longest matching cached prefix. Ordering from least-to-most volatile means a change to USER_PROFILE_CONTEXT
doesn't invalidate STATIC_INSTRUCTIONS
.
tools
array for agentsusage.cache_read_input_tokens
vs cache_creation_input_tokens
β aim for 90%+ hit rate on steady trafficcache_control: {"type": "ephemeral"}
"ttl": "1h"
for sparser trafficFurther reading: