CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-04-28Intermediate

Diagnosing Claude API Prompt Cache Misses — How to Read the usage Field

If your Claude API prompt cache isn't reducing your bill, the usage field is where to start. This guide walks through the five most common reasons cache_read_input_tokens stays at zero and how to fix each one.

claude-api81prompt-caching12troubleshooting87cost-optimization28anthropic12

"I added cache_control last week, but my invoice didn't shrink at all" — this is the single most common message I get from people who just tried Claude's prompt caching. I went through the same thing the first time I shipped it: three days of zero hits before I noticed the cache hadn't been working at all.

The good news is that the failure modes are surprisingly limited, and every diagnosis starts in the same place: the usage object that the API returns on every response. This guide walks through the five misses I run into most often, in the order I check them.

Start with the usage field

Every Claude API response includes a usage object. When you have prompt caching enabled, two extra fields appear there.

import anthropic
 
client = anthropic.Anthropic()
 
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "You are an experienced technical writer." * 200,  # ~2,000 tokens
            "cache_control": {"type": "ephemeral"},
        }
    ],
    messages=[{"role": "user", "content": "Hello"}],
)
 
print(response.usage)
# Usage(
#   input_tokens=8,
#   cache_creation_input_tokens=2103,
#   cache_read_input_tokens=0,
#   output_tokens=42
# )

Three numbers matter here.

  • input_tokens: tokens that bypass the cache and get re-read every request
  • cache_creation_input_tokens: tokens written to the cache on this request (billed at 1.25× standard for the 5-minute TTL, 2× for the 1-hour TTL — but only on first write)
  • cache_read_input_tokens: tokens served from the cache (billed at 0.1× standard)

A working cache means: from the second request onward, cache_read_input_tokens is positive and cache_creation_input_tokens drops toward zero. If you keep firing identical prompts and these numbers never shift, something is wrong.

Cause #1: The prefix is below the minimum token count

Claude enforces a minimum number of tokens for a prefix to be eligible for caching. As of April 2026:

  • Claude Sonnet 4.6 / Claude Opus 4.6: 1,024 tokens
  • Claude Haiku 4.5 family: 2,048 tokens

If your cached prefix is shorter than that, the server quietly skips caching. cache_creation_input_tokens stays at zero, input_tokens keeps climbing, and you wonder why nothing is happening.

This is the trap I fell into first — a 600-token system prompt with cache_control attached and lots of confused log diving. Run your prefix through the token counter before assuming the cache is broken. If you're under the threshold, your next move isn't caching; it's compressing the prompt itself.

Cause #2: cache_control is in the wrong place

Prompt caching treats every block up to and including the one with cache_control as a single cache key. The cache_control marker has to sit on the last block of the static prefix.

# ❌ Wrong: cache_control sits after a dynamic block
system=[
    {"type": "text", "text": SYSTEM_INSTRUCTION},
    {"type": "text", "text": f"Today is {today}"},  # changes daily
    {"type": "text", "text": "Output format: ...",
     "cache_control": {"type": "ephemeral"}},
]

That layout pulls today's date into the cache key, so the cache rotates every midnight and you never get a hit. Move dynamic content after the cached block.

# ✅ Correct: dynamic block comes after the cached prefix
system=[
    {"type": "text", "text": SYSTEM_INSTRUCTION},
    {"type": "text", "text": "Output format: ...",
     "cache_control": {"type": "ephemeral"}},
    {"type": "text", "text": f"Today is {today}"},  # outside the cache
]

The same rule applies to tools. Putting cache_control on the last entry of your tools array caches every tool definition above it as one prefix.

Cause #3: The TTL has expired

The default cache TTL is 5 minutes. If five minutes elapse without a request that hits the same prefix, the cache is gone and the next request pays the write cost again. For low-frequency batch jobs or chatbots where users idle for ten minutes between turns, the cache rarely survives long enough to pay back its write premium.

The 1-hour TTL that became generally available in 2026 is the right tool for these workloads. Writes cost 2× standard instead of 1.25×, but the longer survival makes it cheaper overall when access is sparse.

system=[
    {
        "type": "text",
        "text": LARGE_SYSTEM_PROMPT,
        "cache_control": {"type": "ephemeral", "ttl": "1h"},
    }
]

Log the ratio of cache_read_input_tokens to cache_creation_input_tokens over a full day before and after the change so you have evidence the switch was worth the higher write premium.

Cause #4: A dynamic value sneaks into the cached prefix

This is the bug I keep making. A "static" prefix turns out to contain something that quietly drifts on every request.

Patterns I've seen in production code:

  • A timestamp like Current time: 2026-04-28T09:00:34Z injected at the top of the system prompt
  • A user ID embedded in the system prompt to "remind" Claude who it's talking to
  • A per-session tracking UUID baked into the cached prefix
  • Inconsistent line endings (\n vs. \r\n) across environments

The fastest way to catch these is to hash the cached portion before you send it.

import hashlib
 
def cache_key_hash(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
 
prefix = "".join(b["text"] for b in system_blocks_until_cache_control)
print(f"cache prefix hash: {cache_key_hash(prefix)}")

If the hash changes between requests that ought to be identical, the cause is in your prefix construction. Diff two consecutive prefixes side by side; the offending field tends to be obvious. This single trick saved me half a day chasing a phantom Anthropic-side issue when the real culprit was a request timestamp my own code was adding.

Cause #5: Tool ordering isn't deterministic

If you cache a tools array and still see misses, look at the ordering. Claude compares cache keys as raw bytes, so the same set of tools in a different order is a different cache.

When you build the array from a Python dict whose insertion order depends on runtime conditions, the array shuffles between requests and the cache rotates with it. Pin the ordering with an explicit list.

TOOL_ORDER = ["search_docs", "execute_sql", "send_email"]
 
def build_tools(enabled: set[str]) -> list[dict]:
    # Always emit tools in a fixed order
    return [TOOL_DEFINITIONS[name] for name in TOOL_ORDER if name in enabled]

If different users genuinely have different tool sets, the cache hit rate has a hard ceiling no matter what you do. The realistic design is to split tools into "common" and "user-specific" blocks, attach cache_control only to the common block, and append the per-user tools after it.

A diagnostic script: three calls in a row

Here's the small script I keep around for triage. It fires the same prompt three times and prints how usage evolves.

import anthropic
from anthropic.types import TextBlockParam
 
client = anthropic.Anthropic()
 
SYSTEM_PROMPT = open("system_prompt.txt").read()  # must be >= 1,024 tokens
 
def diagnose():
    for i in range(3):
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=64,
            system=[
                TextBlockParam(
                    type="text",
                    text=SYSTEM_PROMPT,
                    cache_control={"type": "ephemeral"},
                )
            ],
            messages=[{"role": "user", "content": f"test {i}"}],
        )
        u = resp.usage
        print(
            f"req {i}: input={u.input_tokens} "
            f"create={u.cache_creation_input_tokens} "
            f"read={u.cache_read_input_tokens}"
        )
 
diagnose()

Expected output:

req 0: input=8 create=2103 read=0
req 1: input=8 create=0    read=2103
req 2: input=8 create=0    read=2103

If read becomes positive on req 1, caching is working. If it's still zero, walk through causes #1 to #5 in order — they cover almost every miss I've seen in real codebases.

For the design side of caching at scale, see my notes on cutting our monthly Claude API bill in half with prompt caching and the deeper production guide combining prompt caching with Token-Efficient Tool Use. Pair both with API rate limit best practices when you start hitting throughput limits.

What "working" looks like once it's wired up

Once your cache is firing reliably, you'll see three patterns in your dashboards.

First, the per-request input token cost drops to roughly one-tenth of its old value for any prefix you cached, with brief spikes back to full cost every time the TTL window rolls over and a new write happens. If you're charting cost in a grid, the daily shape becomes a flat line with a few small bumps rather than the old constant ceiling.

Second, the ratio of cache_read_input_tokens to cache_creation_input_tokens tells you whether your TTL choice is right for the traffic shape. Anything above 10:1 read-to-write is a healthy steady state. If the ratio sits below 3:1, your write premium is eating most of the savings — that's usually a TTL problem (cause #3) or a prefix that drifts more than you realized (cause #4).

Third, latency improves measurably on cached prefixes — usually a 30 to 50 percent drop in time-to-first-token for the same prompt. This is the side benefit nobody talks about: prompt caching isn't only a cost feature, it's a UX feature for any product where users wait on the first chunk.

Wire those three signals into the same dashboard you already use for error rates and rate-limit headroom. Once the team can see them, regressions get caught the same week they ship rather than the next billing cycle.

What to do next

Drop the diagnostic script into one of your live services for two minutes and watch whether cache_read_input_tokens moves. If it doesn't, start at cause #1 and work down — that's the shortest path to a fix.

Caching isn't a feature you set and forget. It's a feature you instrument with usage, then iterate on until the numbers tell you it's working. Get that loop running once and your bill will start cooperating.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API & SDK2026-04-26
How I Cut My Claude API Bill in Half With Prompt Caching
Done right, Anthropic's prompt caching can roughly halve your monthly API spend on workloads with long, repeated system prompts. Here is the design playbook I use after six months of running it in production.
API & SDK2026-06-29
When Context Editing Made My Agent Re-run the Same Search — Field Notes on Clear Boundaries and Cache Invalidation
After turning on Context Editing to auto-clear tool results, the agent forgot what it had just read, re-ran the same tool, and the cache rebuilt every turn so costs went up. Field notes on instrumenting the silent regression and setting trigger, keep, and clear_at_least from measured data.
API & SDK2026-06-24
I Edited One Line of a Tool Description and the Whole Prompt Cache Rebuilt — Where to Place cache_control Breakpoints
Hit rate suddenly flatlined at zero because a volatile block sat upstream of stable ones. This walks through how prefix-cache cascade invalidation works, how to reorder blocks from stable to volatile, and where to spend your four cache_control breakpoints — with code and decision tables.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →