{"id":468,"date":"2026-07-16T13:03:50","date_gmt":"2026-07-16T13:03:50","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/claude-prompt-caching-2026-the-setup-that-cut-my-api-bill\/"},"modified":"2026-07-16T13:03:50","modified_gmt":"2026-07-16T13:03:50","slug":"claude-prompt-caching-2026-the-setup-that-cut-my-api-bill","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/claude-prompt-caching-2026-the-setup-that-cut-my-api-bill\/","title":{"rendered":"Claude Prompt Caching in 2026: The Setup That Actually Cut My API Bill"},"content":{"rendered":"<p>Confession: I stared at last month&rsquo;s Anthropic bill, made a small sad noise into my coffee, and then spent the rest of the morning re-reading the <a href=\"https:\/\/platform.claude.com\/docs\/en\/build-with-claude\/prompt-caching\" rel=\"nofollow noopener\" target=\"_blank\">prompt caching docs<\/a> like they owed me money. Turns out, they kind of did.<\/p>\n<p>I&rsquo;d been running a support-triage app on top of Claude for a while. Same 8k-token system prompt, same tool definitions, same handful of policy docs on every single request. I knew about <code>cache_control<\/code>. I&rsquo;d even added it once, six months ago, and then never checked whether it was actually doing anything. Spoiler: it wasn&rsquo;t, because I&rsquo;d put the breakpoint in the wrong spot and Anthropic had quietly changed the default TTL under me.<\/p>\n<p>This is the writeup I wish I&rsquo;d read six months ago. What Claude prompt caching actually does, where I put <code>cache_control<\/code> in real requests, the 5-minute vs 1-hour TTL choice, what broke when I got greedy, and the actual line-item difference on my bill.<\/p>\n<h2 id=\"what-prompt-caching-actually-does-in-one-paragraph\">What prompt caching actually does (in one paragraph)<\/h2>\n<p>You mark a chunk of your prompt with <code>cache_control<\/code>. If the same prefix shows up again within the TTL, Claude serves that portion from a warm cache instead of re-processing it. Cache reads are cheap: about 10% of the input token price. Cache writes cost more than a normal input token, though: roughly 1.25\u00d7 for the 5-minute TTL and 2\u00d7 for the 1-hour TTL. So caching only wins when you&rsquo;ll read the cached block enough times to amortize the write.<\/p>\n<p>The rule of thumb I use, borrowed from Anthropic&rsquo;s own guidance and confirmed by <a href=\"https:\/\/dev.to\/whoffagents\/claude-prompt-caching-in-2026-the-5-minute-ttl-change-thats-costing-you-money-4363\" rel=\"nofollow noopener\" target=\"_blank\">this dev.to writeup on the 2026 TTL shift<\/a>: three reads to break even on 5-minute, five reads to break even on 1-hour. Below that, don&rsquo;t bother.<\/p>\n<p>Here&rsquo;s a Python example using the SDK. Before:<\/p>\n<pre><code class=\"language-python\">from anthropic import Anthropic\n\nclient = Anthropic()\n\nresponse = client.messages.create(\n    model=&quot;claude-sonnet-5&quot;,\n    max_tokens=1024,\n    system=BIG_SYSTEM_PROMPT,  # 6k tokens, same every call\n    messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_msg}],\n)\n<\/code><\/pre>\n<p>And after, with a single cache breakpoint:<\/p>\n<pre><code class=\"language-python\">response = client.messages.create(\n    model=&quot;claude-sonnet-5&quot;,\n    max_tokens=1024,\n    system=[\n        {\n            &quot;type&quot;: &quot;text&quot;,\n            &quot;text&quot;: BIG_SYSTEM_PROMPT,\n            &quot;cache_control&quot;: {&quot;type&quot;: &quot;ephemeral&quot;},\n        }\n    ],\n    messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_msg}],\n)\n<\/code><\/pre>\n<p>That&rsquo;s the whole change. On the first request you pay a small write premium. On every subsequent request inside the TTL window, the system prompt is basically free.<\/p>\n<h2 id=\"where-i-actually-put-cache_control-in-real-requests\">Where I actually put cache_control in real requests<\/h2>\n<p>The docs let you place up to four breakpoints in a single request. That flexibility is a trap if you don&rsquo;t have a plan. Here&rsquo;s the ordering I settled on, from most stable to least stable. Cache the stable stuff first, because a cache hit only counts if everything before the breakpoint also matches.<\/p>\n<ol>\n<li><strong>Tool definitions.<\/strong> Change once a week at most. Always cache.<\/li>\n<li><strong>System prompt.<\/strong> Change on deploys. Always cache.<\/li>\n<li><strong>Long context documents<\/strong> (policy docs, code snippets, retrieved chunks). Cache if the same doc is going to show up in the next few requests.<\/li>\n<li><strong>Multi-turn conversation history.<\/strong> Cache the last-but-one assistant turn if you&rsquo;re doing multi-turn.<\/li>\n<\/ol>\n<p>For a typical support-triage request, my breakpoints look like this:<\/p>\n<pre><code class=\"language-python\">response = client.messages.create(\n    model=&quot;claude-sonnet-5&quot;,\n    max_tokens=1024,\n    tools=[\n        # ... tool defs ...\n        {&quot;cache_control&quot;: {&quot;type&quot;: &quot;ephemeral&quot;}}\n    ],\n    system=[\n        {&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: SYSTEM_PROMPT,\n         &quot;cache_control&quot;: {&quot;type&quot;: &quot;ephemeral&quot;}},\n    ],\n    messages=[\n        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [\n            {&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: POLICY_DOC,\n             &quot;cache_control&quot;: {&quot;type&quot;: &quot;ephemeral&quot;}},\n            {&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: user_msg},\n        ]},\n    ],\n)\n<\/code><\/pre>\n<p>Three breakpoints, one slot in reserve. Anthropic&rsquo;s <a href=\"https:\/\/platform.claude.com\/docs\/en\/agents-and-tools\/tool-use\/tool-use-with-prompt-caching\" rel=\"nofollow noopener\" target=\"_blank\">tool-use-with-prompt-caching guide<\/a> has the pattern for cascading breakpoints across multi-turn tool loops if you need more sophistication than this.<\/p>\n<p>TypeScript is almost identical, with the same shape:<\/p>\n<pre><code class=\"language-typescript\">const response = await client.messages.create({\n  model: &quot;claude-sonnet-5&quot;,\n  max_tokens: 1024,\n  system: [\n    {\n      type: &quot;text&quot;,\n      text: SYSTEM_PROMPT,\n      cache_control: { type: &quot;ephemeral&quot; },\n    },\n  ],\n  messages: [{ role: &quot;user&quot;, content: userMsg }],\n});\n<\/code><\/pre>\n<p>If you&rsquo;re on the Vercel AI SDK, the <code>providerOptions.anthropic.cacheControl<\/code> field maps to the same thing. I wrote up how I use that stack in <a href=\"https:\/\/abrarqasim.com\/blog\/vercel-ai-sdk-v5-what-i-actually-use-in-real-apps\" rel=\"noopener\">my post on the Vercel AI SDK v5<\/a>.<\/p>\n<h2 id=\"the-5-minute-vs-1-hour-ttl-choice\">The 5-minute vs 1-hour TTL choice<\/h2>\n<p>The default TTL is 5 minutes. If your traffic is bursty, like a customer opening a chat and sending 10 messages in a minute, 5 minutes is fine. Cheap writes, hits come easy.<\/p>\n<p>The 1-hour TTL costs about 2\u00d7 on writes but keeps the cache warm through longer gaps. You opt into it per breakpoint:<\/p>\n<pre><code class=\"language-python\">&quot;cache_control&quot;: {&quot;type&quot;: &quot;ephemeral&quot;, &quot;ttl&quot;: &quot;1h&quot;}\n<\/code><\/pre>\n<p>I use 1-hour on:<br \/>\n&#8211; Batch pipelines where I know I&rsquo;ll hit the same system prompt every few minutes for an hour.<br \/>\n&#8211; Long-running evals where each iteration takes 4-6 minutes and I don&rsquo;t want a 5-minute window ticking down every time.<br \/>\n&#8211; The tool definitions in my agent loops, which get called dozens of times over the course of a session but with unpredictable gaps.<\/p>\n<p>I stick with 5-minute on:<br \/>\n&#8211; Interactive chat where cache hits are basically guaranteed inside a session.<br \/>\n&#8211; Anything where the &ldquo;outer&rdquo; prompt (tools + system) is small enough that a re-write is cheaper than paying the 1-hour premium.<\/p>\n<p>One thing to watch: Anthropic changed the default TTL from 60 minutes to 5 minutes for Claude Code in early 2026, and it caused a lot of quiet cost regressions. If you have <code>ENABLE_PROMPT_CACHING_1H=1<\/code> set anywhere in an old shell profile, know what it&rsquo;s doing.<\/p>\n<h2 id=\"what-broke-when-i-got-greedy\">What broke when I got greedy<\/h2>\n<p>Three anti-patterns I ran into, in escalating order of expensive:<\/p>\n<p><strong>Cache-busting user data in the wrong slot.<\/strong> I once concatenated the user&rsquo;s timestamp into the system prompt &ldquo;for context&rdquo;. Every request had a unique system prompt, so every request was a cache miss with a write premium. My bill went <em>up<\/em>. The fix is obvious in retrospect: keep dynamic data in the user message, not the system prompt.<\/p>\n<p><strong>Breakpoints on tiny blocks.<\/strong> I tried caching a 200-token instruction block and paid the write premium on every call. Anthropic requires a minimum block size (1024 tokens for Sonnet, 2048 for Opus) for caching to activate at all. Smaller blocks are silently ignored. If you&rsquo;re not sure, log the <code>cache_creation_input_tokens<\/code> and <code>cache_read_input_tokens<\/code> fields on the response and check that they&rsquo;re both non-zero over a window of requests.<\/p>\n<p><strong>Assuming workspace isolation matched my mental model.<\/strong> As of February 2026, caches are isolated per Anthropic workspace on the direct API, not per organization. If you have staging and production in the same org but different workspaces, they don&rsquo;t share cache. That&rsquo;s usually what you want, but I had a batch job that assumed the opposite and paid twice.<\/p>\n<h2 id=\"the-actual-line-item-difference-on-my-bill\">The actual line-item difference on my bill<\/h2>\n<p>Numbers from one of my triage services, comparing April (no caching) to May (with caching landed mid-April) with roughly matched request volumes:<\/p>\n<ul>\n<li>Input tokens (uncached): dropped from 41M to 6M.<\/li>\n<li>Cache read tokens: 0 to 34M.<\/li>\n<li>Cache write tokens: 0 to 1.2M.<\/li>\n<li>Total input spend: dropped about 68%.<\/li>\n<\/ul>\n<p>Cache reads are billed at 10% of the input rate, so those 34M read tokens cost about what 3.4M normal input tokens would. Writes cost more per token but only fire once per new cache prefix. On this workload, my break-even calculation says caching pays for itself within four requests per prefix. I&rsquo;m doing hundreds of requests per prefix per hour.<\/p>\n<p>The <a href=\"https:\/\/www.mager.co\/blog\/2026-04-29-claude-prompt-caching\/\" rel=\"nofollow noopener\" target=\"_blank\">Mager blog on how caching actually works<\/a> has a similar breakdown with different numbers if you want to sanity-check the math against a second workload.<\/p>\n<h2 id=\"what-id-do-this-week-if-i-were-you\">What I&rsquo;d do this week if I were you<\/h2>\n<p>Open your Anthropic usage dashboard. If your app sends more than a handful of requests per session and you don&rsquo;t see any <code>cache_read_input_tokens<\/code> in your response objects, you have easy money on the table. Try this:<\/p>\n<ol>\n<li>Identify the largest static chunk in your requests. Usually it&rsquo;s the system prompt or a set of tool definitions.<\/li>\n<li>Add a single <code>cache_control: {\"type\": \"ephemeral\"}<\/code> block at the end of it.<\/li>\n<li>Log the <code>usage<\/code> object from the response for a day and check the ratio of cache reads to writes. You want reads at least 3\u00d7 writes.<\/li>\n<li>Only then reach for more breakpoints or the 1-hour TTL.<\/li>\n<\/ol>\n<p>Do the smallest version first. Prompt caching is one of those features where the naive implementation captures 80% of the savings, and every additional bit of cleverness has a smaller payoff and a larger blast radius.<\/p>\n<p>If you&rsquo;re doing this alongside a larger LLM-app rebuild, I keep notes on the whole stack (evals, guardrails, cost tracking) over on my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">work page<\/a>. Ping me if you find a break-even calculation I got wrong.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I ran Claude&#8217;s prompt caching in production for six months. Here&#8217;s the cache_control setup, TTL trade-offs, and the anti-patterns that burned me.<\/p>\n","protected":false},"author":2,"featured_media":467,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I ran Claude's prompt caching in production for six months. Here's the cache_control setup, TTL trade-offs, and the anti-patterns that burned me.","rank_math_focus_keyword":"claude prompt caching","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[4,520],"tags":[521,524,523,467,5,527,522,525,526,403],"class_list":["post-468","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","category-llms","tag-anthropic","tag-api-cost-optimization","tag-cache-control","tag-claude","tag-llm","tag-llm-engineering","tag-prompt-caching","tag-python-sdk","tag-typescript-sdk","tag-vercel-ai-sdk-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/468","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=468"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/468\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/467"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=468"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=468"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=468"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}