Skip to content

How Claude Code Prompt Caching Works and When It Expires

Problem

I typed “hey” in Claude Code. Just one word. Then I saw this:

Usage display
Usage: 22% of monthly allocation consumed

I stared at my screen in disbelief. How could a single word - three characters - burn through almost a quarter of my Pro subscription?

When I checked Reddit, I found I wasn’t alone. A user posted the exact same confusion, showing that even minimal messages were consuming massive chunks of their usage limits.

I needed to understand what was happening behind the scenes.

What I discovered

My first assumption was wrong. I thought Claude measured usage by “question difficulty” - simple questions should cost less, complex analysis should cost more.

But Claude doesn’t measure difficulty. It measures tokens - and every message re-sends the entire context window.

Here’s what actually gets sent when I type “hey”:

Token breakdown for 'hey' message
What Claude Code sends with every message:
┌─────────────────────────────────────────────────────────────┐
│ System prompts (CLAUDE.md, project rules) ~10K-50K tokens│
│ Tool definitions (every available tool) ~5K-20K tokens│
│ Conversation history (all previous messages) ~varies │
│ My message: "hey" ~1 token │
├─────────────────────────────────────────────────────────────┤
│ Total per message: ~15K-70K+ │
└─────────────────────────────────────────────────────────────┘

The “hey” was just 1 token. But I was paying for everything else too.

How prompt caching saved me

This is where I learned about Anthropic’s prompt caching feature. The idea is simple: instead of reprocessing the same static content every time, cache it on Anthropic’s servers.

Without vs With caching
WITHOUT CACHING (every message):
┌──────────────────┐
│ System prompts │──→ Full token cost every time
│ Tool definitions │──→ Full token cost every time
│ My message │──→ Full token cost
└──────────────────┘
Total: ~15K-70K tokens × 100% cost
WITH CACHING (warm session):
┌──────────────────┐
│ System prompts │──→ Cache READ (90% cheaper!)
│ Tool definitions │──→ Cache READ (90% cheaper!)
│ My message │──→ Full token cost
└──────────────────┘
Total: ~15K-70K tokens × 10% cost (after initial cache)

The first message in a session pays a premium to write the cache (1.25x normal cost). But every subsequent message reads from cache at 90% discount.

The TTL trap: When cache expires

Here’s where I made my second mistake. I assumed the cache would last for my entire work session.

Wrong.

The cache has a time-to-live (TTL) that depends on my subscription tier:

Cache TTL by subscription tier
┌─────────────────┬────────────────┬─────────────────────────┐
│ Plan │ Cache TTL │ What this means │
├─────────────────┼────────────────┼─────────────────────────┤
│ Pro ($20/mo) │ 5 minutes │ Cache dies quickly │
│ Max ($200/mo) │ 1 hour │ Cache survives longer │
└─────────────────┴────────────────┴─────────────────────────┘

As a Pro user, if I took a 6-minute break between messages, my cache expired. The next message had to rebuild everything from scratch - paying that 1.25x cache-write premium again.

This explained my 22% usage for “hey”:

  • My session had been idle for more than 5 minutes
  • Cache expired
  • Full context reprocessing at full cost
  • Plus a new cache-write fee

How cache control works

When I dug into the API documentation, I found that caching isn’t automatic. You have to explicitly mark content for caching:

basic_caching.py
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": "You are an AI coding assistant...",
},
{
"type": "text",
"text": "<large_codebase_context>", # Static content
"cache_control": {"type": "ephemeral"} # Mark for caching
}
],
messages=[{"role": "user", "content": "Explain this function"}],
)

The cache_control parameter tells Anthropic: “Cache everything up to this point.”

Understanding the API response

When I examined the response from cached requests, I found useful metrics:

api_response.json
{
"usage": {
"input_tokens": 100,
"cache_read_input_tokens": 10000,
"cache_creation_input_tokens": 500,
"output_tokens": 500,
"cache_creation": {
"ephemeral_5m_input_tokens": 456,
"ephemeral_1h_input_tokens": 100
}
}
}

The key fields:

  • cache_read_input_tokens: Tokens read from cache (90% cheaper!)
  • cache_creation_input_tokens: One-time cache write cost (1.25x)
  • ephemeral_5m_input_tokens: Standard 5-minute cache
  • ephemeral_1h_input_tokens: Extended 1-hour cache

The TTL refresh mechanism

Here’s a detail I initially misunderstood. The cache TTL doesn’t start once and then expire after X minutes.

It refreshes each time cached content is accessed.

TTL refresh behavior
Timeline (Pro plan with 5-minute TTL):
Message 1 at 0:00 → Cache created (expires at 0:05)
Message 2 at 0:03 → Cache READ, TTL refreshes (now expires at 0:08)
Message 3 at 0:06 → Cache READ, TTL refreshes (now expires at 0:11)
[Idle 6 minutes]
Message 4 at 0:17 → Cache EXPIRED, full cost again

This means active sessions maintain their cache indefinitely. The problem is idle time - if I step away for a coffee break longer than my TTL, I pay the penalty.

When cache breaks

I also discovered that certain changes invalidate the cache entirely:

Cache invalidation triggers
Cache breaks when:
├── tool_choice parameter changes
├── Images are added or removed anywhere in prompt
├── Cached sections aren't identical across calls
└── More than 20 content blocks exist before cache checkpoint

This means I can’t modify tool configurations mid-session without paying the cache-rebuild cost.

My optimization strategy

After understanding all this, I changed how I use Claude Code:

  1. Stay active in sessions - The TTL refreshes with each use, so continuous work keeps the cache warm

  2. Batch related tasks - Instead of spreading work across multiple sessions, I complete related tasks in one sitting

  3. Upgrade for long sessions - If I need to work on complex projects over hours, the Max plan’s 1-hour TTL is worth it

  4. Monitor cache efficiency - I check the API response to see cache hit rates:

cache_monitor.py
def check_cache_efficiency(response):
total_input = response.usage.input_tokens
cache_read = response.usage.cache_read_input_tokens or 0
cache_created = response.usage.cache_creation_input_tokens or 0
if cache_read > 0:
efficiency = cache_read / (total_input + cache_read) * 100
print(f"Cache hit! {efficiency:.1f}% from cache (90% cheaper)")
elif cache_created > 0:
print(f"Cache created: {cache_created} tokens (one-time 1.25x cost)")
else:
print("No caching in effect - full cost")
return response

Why this design makes sense

Once I understood the economics, the pricing model became logical:

  • Computing costs correlate with tokens processed, not question complexity
  • Processing “hey” with 50K tokens of context costs similar to processing “analyze this codebase” with 50K tokens
  • The GPU doesn’t care about question difficulty - it performs the same matrix operations
  • System prompts and tool definitions ensure quality and safety

The 90% cost reduction from caching is Anthropic passing infrastructure savings to users who maintain warm sessions.

Summary

I discovered that saying “hey” cost me 22% of my usage because:

  1. Every message re-sends the entire context window (system prompts, tools, history)
  2. My cache had expired due to the 5-minute TTL on Pro plans
  3. The full context had to be reprocessed at full cost

Key takeaways:

  • Prompt caching reduces costs by up to 90% for cached content
  • Pro plan TTL is 5 minutes; Max plan TTL is 1 hour
  • Cache TTL refreshes on each use - active sessions stay warm
  • Idle time longer than your TTL means full reprocessing cost
  • Cache writes cost 1.25x, cache reads cost ~90% less

The “hey” message wasn’t expensive because of the word - it was expensive because my session context was cold and had to be rebuilt.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments