{"slug": "stop-paying-for-the-same-tokens-twice-a-practical-guide-to-prompt-caching", "title": "Stop Paying for the Same Tokens Twice: A Practical Guide to Prompt Caching", "summary": "A developer's guide explains how prompt caching in Anthropic's Claude API can cut input costs by up to 90% on repeated conversation prefixes, requiring only a single `cache_control` parameter. The technique caches tokens from the start of a request up to a marked block, with cache reads priced at 0.1x the base input rate, and it also improves response speed and effective throughput.", "body_md": "You've built a chatbot. Every turn, you re-send the whole conversation — the 8,000-token system prompt, the uploaded PDF, the 15 messages of history — just so the model can answer \"and what about Mars?\"\n\nThe model re-reads all of it. Every. Single. Time. You pay full price for all of it. Every. Single. Time.\n\nPrompt caching fixes this. It's roughly one extra line of JSON, and it can cut your input costs by ~90% on the repeated part while making responses noticeably faster.\n\nAdd `cache_control`\n\nat the top level of your request:\n\n```\nresponse = client.messages.create(\n    model=\"claude-opus-5\",\n    max_tokens=1024,\n    cache_control={\"type\": \"ephemeral\"},   # <-- this is the whole trick\n    system=\"You are an AI assistant tasked with analyzing literary works...\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"Analyze the major themes in 'Pride and Prejudice'.\"}\n    ],\n)\nprint(response.usage)\n```\n\nThat's **automatic caching**. The API caches everything up to and including the last cacheable block in your request. Next time you send a request that starts with the same content, that prefix is read from cache instead of reprocessed.\n\nSame thing in curl, if that's more your speed:\n\n```\ncurl https://api.anthropic.com/v1/messages \\\n  -H \"content-type: application/json\" \\\n  -H \"x-api-key: $ANTHROPIC_API_KEY\" \\\n  -H \"anthropic-version: 2023-06-01\" \\\n  -d '{\n    \"model\": \"claude-opus-5\",\n    \"max_tokens\": 1024,\n    \"cache_control\": {\"type\": \"ephemeral\"},\n    \"system\": \"You are an AI assistant tasked with analyzing literary works.\",\n    \"messages\": [{\"role\": \"user\", \"content\": \"Analyze the major themes in Pride and Prejudice.\"}]\n  }'\n```\n\nThis is the single most important thing to internalize.\n\nYour prompt is read in a fixed order:\n\n```\ntools  →  system  →  messages\n```\n\nCaching works on **prefixes** of that sequence. When you mark a block with `cache_control`\n\n, you're saying: *\"cache everything from the start of the request up to and including this block.\"*\n\nWhich means:\n\nThree price tiers instead of one:\n\n| Multiplier vs. base input | |\n|---|---|\n5-minute cache write\n|\n1.25× |\n1-hour cache write\n|\n2× |\nCache read (hit) |\n0.1× |\n\nSo for Claude Opus 5 ($5/MTok input):\n\n```\n\"usage\": {\n  \"cache_read_input_tokens\": 100000,\n  \"cache_creation_input_tokens\": 0,\n  \"input_tokens\": 50\n}\n```\n\n`input_tokens`\n\nis **not** your total input. It's only the tokens *after* your last cache breakpoint. Total is:\n\n```\ntotal = cache_read_input_tokens + cache_creation_input_tokens + input_tokens\n```\n\nSo the example above processed 100,050 tokens, not 50. Handy side effect: cache hits don't count against your rate limits the way fresh input does, so effective throughput goes up too.\n\n**Quick sanity check:** if both `cache_creation_input_tokens`\n\nand `cache_read_input_tokens`\n\nare `0`\n\n, nothing was cached. Most likely you're under the minimum length (see below) — the API won't error, it just silently skips caching.\n\nPrompts shorter than a per-model floor simply won't cache:\n\n| Model | Minimum cacheable tokens |\n|---|---|\n| Opus 5, Fable 5, Mythos 5 | 512 |\n| Opus 4.8, Sonnet 5 / 4.6 / 4.5 | 1,024 |\n| Opus 4.7, Haiku 3.5 | 2,048 |\n| Opus 4.6, Opus 4.5, Haiku 4.5 | 4,096 |\n\nIf you're *just* under the line, it's often worth padding the cached section (more examples, more context) to get over it. Cache reads are cheap enough that the extra tokens pay for themselves.\n\nWith automatic caching, the breakpoint walks forward on its own as the conversation grows:\n\n| Request | What happens |\n|---|---|\n| 1 |\n`System + U1 + A1 + **U2**` → everything written to cache |\n| 2 |\n`... + A2 + **U3**` → System→U2 read from cache, A2+U3 written |\n| 3 |\n`... + A3 + **U4**` → System→U3 read from cache, A3+U4 written |\n\nNo bookkeeping. No moving markers around. Each turn reads the whole prior conversation from cache and only writes the new bit. This is the single best default for chat apps.\n\nPut `cache_control`\n\non individual blocks when different parts of your prompt change at different rates. You get up to **4 breakpoints**.\n\nClassic RAG-agent layout:\n\n```\n{\n  \"tools\": [ /* ... */, { \"name\": \"get_document\", \"cache_control\": {\"type\": \"ephemeral\"} } ],\n  \"system\": [\n    { \"type\": \"text\", \"text\": \"You are a research assistant...\", \"cache_control\": {\"type\": \"ephemeral\"} },\n    { \"type\": \"text\", \"text\": \"# Knowledge Base\\n## Doc 1...\", \"cache_control\": {\"type\": \"ephemeral\"} }\n  ],\n  \"messages\": [\n    { \"role\": \"user\", \"content\": [\n      { \"type\": \"text\", \"text\": \"Tell me about Perseverance.\", \"cache_control\": {\"type\": \"ephemeral\"} }\n    ]}\n  ]\n}\n```\n\nFour independent segments:\n\nHere's the bug I want you to remember, because it's expensive and silent.\n\nYour prompt: blocks 1–5 are a big static system context. Block 6 is `f\"[{timestamp}] {user_message}\"`\n\n. You put `cache_control`\n\non block 6, because it's the end and that seems right.\n\n`cache_control`\n\nto A related gotcha. When looking for a cache hit, the system checks your breakpoint's position and then walks backward — but only **20 blocks**.\n\nDefault TTL is **5 minutes**, and it refreshes for free every time you hit the cache. An active chat session basically keeps itself warm.\n\n```\n\"cache_control\": { \"type\": \"ephemeral\", \"ttl\": \"1h\" }\n```\n\nReach for 1h when:\n\nLatency-sensitive app? The first user of the day eats the cache-miss penalty. Unless you warm it up first:\n\n```\nSYSTEM_PROMPT = [{\n    \"type\": \"text\",\n    \"text\": \"You are an expert software engineer...\",\n    \"cache_control\": {\"type\": \"ephemeral\"},\n}]\ndef prewarm_cache():\n    client.messages.create(\n        model=\"claude-opus-5\",\n        max_tokens=0,                                  # no output generated\n        system=SYSTEM_PROMPT,\n        messages=[{\"role\": \"user\", \"content\": \"warmup\"}],\n    )\n```\n\n`max_tokens: 0`\n\nreads your prompt in, writes the cache at your breakpoint, and returns immediately with an empty `content`\n\narray and `stop_reason: \"max_tokens\"`\n\n. Zero output tokens billed. (You still pay the cache write, naturally.)\n\nTwo things to get right:\n\n`\"warmup\"`\n\nplaceholder — otherwise the entry is keyed to the placeholder and real traffic never hits it. This is why pre-warming needs an explicit breakpoint rather than automatic caching.`effort`\n\nsetting as your real requests. Those get rendered into the prompt, so a mismatched pre-warm writes an entry nobody uses.\n`max_tokens: 0`\n\nis rejected with `stream: true`\n\n, extended thinking, structured outputs, forced `tool_choice`\n\n, or inside a Batches request.Cache hits need a **100% byte-identical** prefix. Things that invalidate:\n\n| Change | Blast radius |\n|---|---|\n| Tool definitions | Everything |\n| Toggling web search / citations | System + messages |\n| Switching fast mode | System + messages |\n`tool_choice` |\nMessages |\n| Adding/removing images anywhere | Messages |\nThinking config or `effort`\n|\nMessages (and sometimes more) |\n\nOne sneaky one: some languages (**Go, Swift**) randomize map key order when serializing JSON. If your `tool_use`\n\nblocks come out with shuffled keys, your cache never hits and you'll have no idea why. Pin the ordering.\n\nAlso: caches are isolated per organization, and per workspace on the Claude API. And a cache entry only becomes available *after the first response begins* — so firing 10 parallel requests with the same prefix gives you 10 misses. Send one, wait, then fan out.\n\nCache not hitting? Run down this list:\n\n`tool_choice`\n\n, image presence, thinking config, and `effort`\n\n*Got a caching setup that surprised you — good or bad? Drop it in the comments.* 🚀", "url": "https://wpnews.pro/news/stop-paying-for-the-same-tokens-twice-a-practical-guide-to-prompt-caching", "canonical_source": "https://dev.to/mukul_sharma_61fc4dd6f9d8/stop-paying-for-the-same-tokens-twice-a-practical-guide-to-prompt-caching-4938", "published_at": "2026-08-05 19:03:27+00:00", "updated_at": "2026-08-05 19:26:26.803227+00:00", "lang": "en", "topics": ["large-language-models", "developer-tools", "ai-infrastructure"], "entities": ["Anthropic", "Claude Opus 5", "Claude Fable 5", "Claude Mythos 5", "Claude Sonnet 5", "Claude Haiku 3.5"], "alternates": {"html": "https://wpnews.pro/news/stop-paying-for-the-same-tokens-twice-a-practical-guide-to-prompt-caching", "markdown": "https://wpnews.pro/news/stop-paying-for-the-same-tokens-twice-a-practical-guide-to-prompt-caching.md", "text": "https://wpnews.pro/news/stop-paying-for-the-same-tokens-twice-a-practical-guide-to-prompt-caching.txt", "jsonld": "https://wpnews.pro/news/stop-paying-for-the-same-tokens-twice-a-practical-guide-to-prompt-caching.jsonld"}}