{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Prompt caching with the Claude API\n",
"\n",
"Prompt caching lets you store and reuse context within your prompts, reducing latency by >2x and costs by up to 90% for repetitive tasks.\n",
"\n",
"There are two ways to enable prompt caching:\n",
"\n",
"- **Automatic caching** (recommended): Add a single `cache_control` field at the top level of your request. The system automatically manages cache breakpoints for you.\n",
"- **Explicit cache breakpoints**: Place `cache_control` on individual content blocks for fine-grained control over exactly what gets cached.\n",
"\n",
"This cookbook demonstrates both approaches, starting with the simpler automatic method."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"%pip install --upgrade 'anthropic>=0.83.0' bs4 requests python-dotenv --quiet"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"import anthropic\n",
"import requests\n",
"from bs4 import BeautifulSoup\n",
"from dotenv import load_dotenv\n",
"\n",
"load_dotenv()\n",
"client = anthropic.Anthropic()\n",
"MODEL_NAME = \"claude-sonnet-5-5\"\n",
"\n",
"# Unique prefix to ensure we don't hit a stale cache from a previous run\n",
"TIMESTAMP = int(time.time())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's fetch the full text of *Pride and Prejudice* (~187k tokens) to use as our large context."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Fetched 737526 characters from the book.\n",
"First 500 characters:\n",
"The Project Gutenberg eBook of Pride and Prejudice\n",
"This ebook is for the use of anyone anywhere in the United States and\n",
"most other parts of the world at no cost and with almost no restrictions\n",
"whatsoever. You may copy it, give it away or re-use it under the terms\n",
"of the Project Gutenberg License included with this ebook or online\n",
"at www.gutenberg.org. If you are not located in the United States,\n",
"you will have to check the laws of the country where you are located\n",
"before using this eBook.\n",
"Title:\n"
]
}
],
"source": [
"def fetch_article_content(url):\n",
" response = requests.get(url, timeout=30)\n",
" soup = BeautifulSoup(response.content, \"html.parser\")\n",
"\n",
" for script in soup([\"script\", \"style\"]):\n",
" script.decompose()\n",
"\n",
" text = soup.get_text()\n",
" lines = (line.strip() for line in text.splitlines())\n",
" chunks = (phrase.strip() for line in lines for phrase in line.split(\" \"))\n",
" text = \"\\n\".join(chunk for chunk in chunks if chunk)\n",
"\n",
" return text\n",
"\n",
"\n",
"book_url = \"https://www.gutenberg.org/cache/epub/1342/pg1342.txt\"\n",
"book_content = fetch_article_content(book_url)\n",
"\n",
"print(f\"Fetched {len(book_content)} characters from the book.\")\n",
"print(\"First 500 characters:\")\n",
"print(book_content[:500])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We'll also define small helpers to extract the response text and print usage stats:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"def response_text(response):\n",
" \"\"\"Return the text of a response. Responses can start with a thinking block, so this reads\n",
" the text blocks rather than content[0].\"\"\"\n",
" return \"\".join(block.text for block in response.content if block.type == \"text\")\n",
"\n",
"\n",
"def print_usage(response, elapsed):\n",
" \"\"\"Print token usage and timing for an API response.\"\"\"\n",
" usage = response.usage\n",
" cache_create = getattr(usage, \"cache_creation_input_tokens\", 0)\n",
" cache_read = getattr(usage, \"cache_read_input_tokens\", 0)\n",
"\n",
" print(f\" Time: {elapsed:.2f}s\")\n",
" print(f\" Input tokens: {usage.input_tokens}\")\n",
" print(f\" Output tokens: {usage.output_tokens}\")\n",
" if cache_create:\n",
" print(f\" Cache write tokens: {cache_create}\")\n",
" if cache_read:\n",
" print(f\" Cache read tokens: {cache_read}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Example 1: Automatic caching (single turn)\n",
"\n",
"Automatic caching is the easiest way to get started. Add `cache_control={\"type\": \"ephemeral\"}` at the **top level** of your `messages.create()` call and the system handles the rest — automatically placing the cache breakpoint on the last cacheable block.\n",
"\n",
"We'll compare three scenarios:\n",
"1. **No caching** — baseline\n",
"2. **First cached call** — creates the cache entry (similar timing to baseline)\n",
"3. **Second cached call** — reads from cache (the big speedup)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Baseline: no caching"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Response: Pride and Prejudice\n",
" Time: 4.89s\n",
" Input tokens: 187364\n",
" Output tokens: 8\n"
]
}
],
"source": [
"start = time.time()\n",
"baseline_response = client.messages.create(\n",
" model=MODEL_NAME,\n",
" max_tokens=1024,\n",
" messages=[\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": str(TIMESTAMP)\n",
" + \"\"\n",
" + book_content\n",
" + \"\"\n",
" + \"\\n\\nWhat is the title of this book? Only output the title.\",\n",
" }\n",
" ],\n",
")\n",
"baseline_time = time.time() - start\n",
"\n",
"print(f\"Response: {response_text(baseline_response)}\")\n",
"print_usage(baseline_response, baseline_time)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### First call with automatic caching (cache write)\n",
"\n",
"The only change is the top-level `cache_control` parameter. The first call writes to the cache, so timing is similar to the baseline."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Response: Pride and Prejudice\n",
" Time: 4.28s\n",
" Input tokens: 3\n",
" Output tokens: 8\n",
" Cache write tokens: 187361\n"
]
}
],
"source": [
"start = time.time()\n",
"write_response = client.messages.create(\n",
" model=MODEL_NAME,\n",
" max_tokens=1024,\n",
" cache_control={\"type\": \"ephemeral\"}, # <-- one-line change\n",
" messages=[\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": str(TIMESTAMP)\n",
" + \"\"\n",
" + book_content\n",
" + \"\"\n",
" + \"\\n\\nWhat is the title of this book? Only output the title.\",\n",
" }\n",
" ],\n",
")\n",
"write_time = time.time() - start\n",
"\n",
"print(f\"Response: {response_text(write_response)}\")\n",
"print_usage(write_response, write_time)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Second call with automatic caching (cache hit)\n",
"\n",
"Same request again. This time the cached prefix is reused, so you should see a significant speedup."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Response: Pride and Prejudice\n",
" Time: 1.48s\n",
" Input tokens: 3\n",
" Output tokens: 8\n",
" Cache read tokens: 187361\n",
"\n",
"==================================================\n",
"COMPARISON\n",
"==================================================\n",
"No caching: 4.89s\n",
"Cache write: 4.28s\n",
"Cache hit: 1.48s\n",
"Speedup: 3.3x\n"
]
}
],
"source": [
"start = time.time()\n",
"hit_response = client.messages.create(\n",
" model=MODEL_NAME,\n",
" max_tokens=1024,\n",
" cache_control={\"type\": \"ephemeral\"},\n",
" messages=[\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": str(TIMESTAMP)\n",
" + \"\"\n",
" + book_content\n",
" + \"\"\n",
" + \"\\n\\nWhat is the title of this book? Only output the title.\",\n",
" }\n",
" ],\n",
")\n",
"hit_time = time.time() - start\n",
"\n",
"print(f\"Response: {response_text(hit_response)}\")\n",
"print_usage(hit_response, hit_time)\n",
"\n",
"print(\"\\n\" + \"=\" * 50)\n",
"print(\"COMPARISON\")\n",
"print(\"=\" * 50)\n",
"print(f\"No caching: {baseline_time:.2f}s\")\n",
"print(f\"Cache write: {write_time:.2f}s\")\n",
"print(f\"Cache hit: {hit_time:.2f}s\")\n",
"print(f\"Speedup: {baseline_time / hit_time:.1f}x\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Example 2: Automatic caching in a multi-turn conversation\n",
"\n",
"Automatic caching really shines in multi-turn conversations. The cache breakpoint **automatically moves forward** as the conversation grows — you don't need to manage any markers yourself.\n",
"\n",
"| Request | Cache behavior |\n",
"|---------|----------------|\n",
"| Request 1 | System + User:A cached (write) |\n",
"| Request 2 | System + User:A read from cache; Asst:B + User:C written to cache |\n",
"| Request 3 | System through User:C read from cache; Asst:D + User:E written to cache |"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"==================================================\n",
"Turn 1: What is the title of this novel?\n",
"==================================================\n",
"\n",
"Assistant: The title of this novel is **Pride and Prejudice**, written by **Jane Austen**.\n",
"\n",
" Time: 5.19s\n",
" Input tokens: 3\n",
" Output tokens: 24\n",
" Cache write tokens: 187361\n",
"\n",
"==================================================\n",
"Turn 2: Who are Mr. and Mrs. Bennet?\n",
"==================================================\n",
"\n",
"Assistant: Mr. and Mrs. Bennet are a married couple who are central characters in the novel. They live at **Longbourn** and are the parents of **five daughters**: Jane, Elizabeth, Mary, Catherine (Kitty), and Ly...\n",
"\n",
" Time: 8.27s\n",
" Input tokens: 3\n",
" Output tokens: 272\n",
" Cache write tokens: 38\n",
" Cache read tokens: 187361\n",
"\n",
"==================================================\n",
"Turn 3: What is Netherfield Park?\n",
"==================================================\n",
"\n",
"Assistant: **Netherfield Park** is a large estate located near the village of **Longbourn** in Hertfordshire, where the Bennet family lives. It plays an important role in the novel as it is the home that is let ...\n",
"\n",
" Time: 8.74s\n",
" Input tokens: 3\n",
" Output tokens: 300\n",
" Cache write tokens: 283\n",
" Cache read tokens: 187399\n",
"\n",
"==================================================\n",
"Turn 4: What is the main theme of this novel?\n",
"==================================================\n",
"\n",
"Assistant: **Pride and Prejudice** explores several important themes throughout the novel. Here are the main ones:\n",
"\n",
"**1. Pride and Prejudice**\n",
"- The most obvious theme is reflected in the title itself. Mr. Darcy...\n",
"\n",
" Time: 7.06s\n",
" Input tokens: 3\n",
" Output tokens: 300\n",
" Cache write tokens: 315\n",
" Cache read tokens: 187682\n"
]
}
],
"source": [
"system_message = f\"{TIMESTAMP} {book_content} \"\n",
"\n",
"questions = [\n",
" \"What is the title of this novel?\",\n",
" \"Who are Mr. and Mrs. Bennet?\",\n",
" \"What is Netherfield Park?\",\n",
" \"What is the main theme of this novel?\",\n",
"]\n",
"\n",
"conversation = []\n",
"\n",
"for i, question in enumerate(questions, 1):\n",
" print(f\"\\n{'=' * 50}\")\n",
" print(f\"Turn {i}: {question}\")\n",
" print(\"=\" * 50)\n",
"\n",
" conversation.append({\"role\": \"user\", \"content\": question})\n",
"\n",
" start = time.time()\n",
" response = client.messages.create(\n",
" model=MODEL_NAME,\n",
" max_tokens=1024,\n",
" cache_control={\"type\": \"ephemeral\"}, # automatic caching\n",
" system=system_message,\n",
" messages=conversation,\n",
" )\n",
" elapsed = time.time() - start\n",
"\n",
" assistant_reply = response_text(response)\n",
" conversation.append({\"role\": \"assistant\", \"content\": assistant_reply})\n",
"\n",
" print(f\"\\nAssistant: {assistant_reply[:200]}{'...' if len(assistant_reply) > 200 else ''}\")\n",
" print()\n",
" print_usage(response, elapsed)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"After the first turn, nearly 100% of input tokens are read from cache on every subsequent turn. The conversation code is just a plain list of messages — no special `cache_control` markers needed on individual blocks."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Example 3: Explicit cache breakpoints\n",
"\n",
"For more control, you can place `cache_control` directly on individual content blocks. This is useful when:\n",
"\n",
"- You want to cache different sections with different TTLs\n",
"- You need to cache a system prompt independently from message content\n",
"- You want fine-grained control over what gets cached\n",
"\n",
"You can also combine both approaches: use explicit breakpoints for your system prompt while automatic caching handles the conversation.\n",
"\n",
"Below, we place `cache_control` directly on the book content block and manually move the breakpoint forward on each turn."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"==================================================\n",
"Turn 1: What is the title of this novel?\n",
"==================================================\n",
"\n",
"Assistant: The title of this novel is **Pride and Prejudice**, written by **Jane Austen**.\n",
"\n",
" Time: 4.53s\n",
" Input tokens: 3\n",
" Output tokens: 24\n",
" Cache read tokens: 187361\n",
"\n",
"==================================================\n",
"Turn 2: Who are Mr. and Mrs. Bennet?\n",
"==================================================\n",
"\n",
"Assistant: Mr. and Mrs. Bennet are a married couple who are central characters in the novel. They live at **Longbourn** and are the parents of **five daughters**: Jane, Elizabeth (Lizzy), Mary, Catherine (Kitty)...\n",
"\n",
" Time: 7.57s\n",
" Input tokens: 3\n",
" Output tokens: 283\n",
" Cache read tokens: 187399\n",
"\n",
"==================================================\n",
"Turn 3: What is Netherfield Park?\n",
"==================================================\n",
"\n",
"Assistant: **Netherfield Park** is a large estate located near the village of **Longbourn** in Hertfordshire, where the Bennet family lives. It plays an important role in the novel as it is the residence that se...\n",
"\n",
" Time: 6.85s\n",
" Input tokens: 3\n",
" Output tokens: 300\n",
" Cache write tokens: 294\n",
" Cache read tokens: 187399\n",
"\n",
"==================================================\n",
"Turn 4: What is the main theme of this novel?\n",
"==================================================\n",
"\n",
"Assistant: **Pride and Prejudice** explores several interconnected themes throughout the novel. Here are the main ones:\n",
"\n",
"**1. Pride and Prejudice**\n",
"The most central theme is reflected in the title itself. Both M...\n",
"\n",
" Time: 7.00s\n",
" Input tokens: 3\n",
" Output tokens: 300\n",
" Cache write tokens: 315\n",
" Cache read tokens: 187693\n"
]
}
],
"source": [
"class ConversationWithExplicitCaching:\n",
" \"\"\"Multi-turn conversation that manually places cache_control on the last user message.\"\"\"\n",
"\n",
" def __init__(self):\n",
" self.turns = []\n",
"\n",
" def add_user(self, content):\n",
" self.turns.append({\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": content}]})\n",
"\n",
" def add_assistant(self, content):\n",
" self.turns.append({\"role\": \"assistant\", \"content\": [{\"type\": \"text\", \"text\": content}]})\n",
"\n",
" def get_messages(self):\n",
" \"\"\"Return messages with cache_control on the last user message.\"\"\"\n",
" result = []\n",
" last_user_idx = max(i for i, t in enumerate(self.turns) if t[\"role\"] == \"user\")\n",
"\n",
" for i, turn in enumerate(self.turns):\n",
" if i == last_user_idx:\n",
" result.append(\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": [\n",
" {\n",
" \"type\": \"text\",\n",
" \"text\": turn[\"content\"][0][\"text\"],\n",
" \"cache_control\": {\"type\": \"ephemeral\"},\n",
" }\n",
" ],\n",
" }\n",
" )\n",
" else:\n",
" result.append(turn)\n",
"\n",
" return result\n",
"\n",
"\n",
"conv = ConversationWithExplicitCaching()\n",
"\n",
"for i, question in enumerate(questions, 1):\n",
" print(f\"\\n{'=' * 50}\")\n",
" print(f\"Turn {i}: {question}\")\n",
" print(\"=\" * 50)\n",
"\n",
" conv.add_user(question)\n",
"\n",
" start = time.time()\n",
" response = client.messages.create(\n",
" model=MODEL_NAME,\n",
" max_tokens=1024,\n",
" system=[\n",
" {\n",
" \"type\": \"text\",\n",
" \"text\": system_message,\n",
" \"cache_control\": {\"type\": \"ephemeral\"}, # explicit breakpoint on system\n",
" },\n",
" ],\n",
" messages=conv.get_messages(),\n",
" )\n",
" elapsed = time.time() - start\n",
"\n",
" assistant_reply = response_text(response)\n",
" conv.add_assistant(assistant_reply)\n",
"\n",
" print(f\"\\nAssistant: {assistant_reply[:200]}{'...' if len(assistant_reply) > 200 else ''}\")\n",
" print()\n",
" print_usage(response, elapsed)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Choosing an approach\n",
"\n",
"| | Automatic caching | Explicit breakpoints |\n",
"|---|---|---|\n",
"| **Ease of use** | One-line change | Must place and move `cache_control` markers |\n",
"| **Multi-turn** | Breakpoint moves forward automatically | You manage breakpoint placement |\n",
"| **Fine-grained control** | No | Up to 4 independent breakpoints |\n",
"| **Mixed TTLs** | Single TTL for auto breakpoint | Different TTLs per breakpoint |\n",
"| **Combinable** | Yes — automatic + explicit together | Yes |\n",
"\n",
"**Start with automatic caching.** It covers the majority of use cases with minimal effort. Switch to explicit breakpoints only when you need fine-grained control.\n",
"\n",
"### Key details\n",
"\n",
"- **Minimum cacheable length:** model-dependent (for example, 512 tokens on Claude Opus 5.5); see the prompt caching docs linked below. Shorter prefixes are processed normally but not cached.\n",
"- **Cache TTL:** 5 minutes by default (refreshed on each hit). A 1-hour TTL is available at 2x base input price.\n",
"- **Pricing:** Cache writes cost 1.25x base input price. Cache reads cost 0.1x base input price or less, depending on the model.\n",
"- **Breakpoint limit:** Up to 4 explicit breakpoints per request. Automatic caching uses one slot.\n",
"\n",
"For full details, see the [prompt caching documentation](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.12"
}
},
"nbformat": 4,
"nbformat_minor": 4
}