MODULE 6  ยท  Model Serving & APIs

Prompt Caching: How to Cut Your AI Costs by 90%

Every API call re-reads your entire system prompt. Every. Single. Time. Prompt caching fixes this โ€” and slashes your bill.

๐Ÿ“… Mar 2026
โฑ 10 min read
๐ŸŽฏ Episode 38 of 98
Prompt CachingCost OptimizationAPIPerformance
In this episode

Every time you call an LLM API, the model re-reads your entire system prompt. Every. Single. Time.

That 2,000-token system prompt you carefully crafted? The model processes it fresh on every request. Those 50 tool definitions? Re-parsed from scratch. That RAG context you injected? Re-embedded into the model's attention.

For an agent making 20 calls per task, that's 20 ร— 2,000 = 40,000 tokens of repeated system prompt processing. At $2.50/million input tokens, you're paying to re-read the same instructions 20 times.

Prompt caching fixes this. The provider caches the processed form of your prompt prefix, and subsequent requests that share the same prefix skip the processing. Same result, fraction of the cost.

How Prompt Caching Works

The concept is simple:

Request 1:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”

โ”‚ System prompt + tools (2,000t) โ”‚ User msg (50t)โ”‚ โ† Full processing

โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

example
code
Cost: 2,050 tokens ร— full price

Request 2 (same system prompt):

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”

โ”‚ CACHED (2,000t) โ”‚ User msg (60t)โ”‚ โ† Only process new part

โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

example
code
Cost: 2,000 tokens ร— 10% + 60 tokens ร— full price

The provider stores the processed KV-cache (key-value cache from the attention mechanism) for the shared prefix. When a new request starts with the same prefix, it loads the cached state instead of recomputing it.

What Gets Cached

The cache matches from the beginning of the messages array. As long as the prefix is identical (byte-for-byte), the cache hits:

python

Request 1

snippet
code
messages = [
{"role": "system", "content": "You are a helpful assistant..."},  # โ† Cached
    {"role": "user", "content": "What is Python?"}                     # โ† New

]

Request 2 (cache HIT โ€” same system prompt)

snippet
code
messages = [
{"role": "system", "content": "You are a helpful assistant..."},  # โ† Cache hit!
    {"role": "user", "content": "What is JavaScript?"}                 # โ† New

]

Request 3 (cache MISS โ€” different system prompt)

snippet
code
messages = [
{"role": "system", "content": "You are a code reviewer..."},      # โ† Different! Cache miss
    {"role": "user", "content": "Review this code"}

]

โšก

Anthropic's Prompt Caching

Anthropic was first to make prompt caching explicit and developer-controlled. You mark which parts of the prompt should be cached using cache_control breakpoints.

How to Use It

python

import anthropic

snippet
code
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "You are a senior Python developer assistant. " * 100,  # Long system prompt
            "cache_control": {"type": "ephemeral"}  # โ† Cache this!
        }
    ],
    messages=[
        {"role": "user", "content": "Write a hello world function"}
    ]

)

Check cache performance

snippet
code
print(response.usage)

{

"input_tokens": 50,

"cache_creation_input_tokens": 2000, โ† First call: cache created

"cache_read_input_tokens": 0

}

Second call with the same system prompt:

python

Same system prompt, different user message

snippet
code
response2 = client.messages.create(
model="claude-sonnet-4-20250514",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "You are a senior Python developer assistant. " * 100,
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=[
        {"role": "user", "content": "Now write a fibonacci function"}
    ]

)

snippet
code
print(response2.usage)

{

"input_tokens": 50,

"cache_creation_input_tokens": 0,

"cache_read_input_tokens": 2000, โ† Cache HIT! 90% cheaper

}

Anthropic Cache Rules

Rule Detail

Minimum tokens1,024 tokens (Sonnet), 2,048 (Haiku) to be cached
Cache lifetime5 minutes (resets on each hit)
Max breakpoints4 per request
Cache write cost25% more than regular input tokens
Cache read cost90% less than regular input tokens
MatchingExact prefix match, byte-for-byte

Cache Economics

Scenario Without Cache With Cache Savings

1 call$0.003$0.00375 (write penalty)-25% (first call costs more)
5 calls$0.015$0.00375 + 4ร—$0.0003 = $0.0049567%
20 calls$0.060$0.00375 + 19ร—$0.0003 = $0.0094584%
100 calls$0.300$0.00375 + 99ร—$0.0003 = $0.0334589%

The first call is slightly more expensive (cache write fee), but every subsequent call is 90% cheaper. By the 3rd call, you're already saving money.

๐Ÿ”ง

OpenAI's Automatic Caching

OpenAI takes a different approach โ€” automatic caching with no developer action required.

How It Works

python

Just make normal API calls โ€” caching happens automatically

snippet
code
response = client.chat.completions.create(
model="gpt-4o",
    messages=[
        {"role": "system", "content": very_long_system_prompt},  # Auto-cached
        {"role": "user", "content": "Hello!"}
    ]

)

Check if cache was used

snippet
code
print(response.usage)

{

"prompt_tokens": 2050,

"completion_tokens": 20,

"prompt_tokens_details": {

"cached_tokens": 2000 โ† Automatic cache hit!

}

}

No cache_control, no explicit breakpoints. OpenAI detects repeated prefixes and caches them automatically.

OpenAI Cache Rules

Rule Detail

Minimum tokens1,024 tokens
Cache granularity128-token blocks
Cache lifetime~5-10 minutes (dynamic)
Cache read cost50% discount (not 90% like Anthropic)
Cache write costFree (no write penalty)
Developer controlNone โ€” fully automatic

Anthropic vs OpenAI Caching

Feature Anthropic OpenAI

Control Explicit (cache_control) Automatic

Read discount90%50%
Write penalty25% extraNone
Minimum tokens1,024-2,0481,024
BreakpointsUp to 4N/A
Best forLong, stable system promptsEverything (no effort)

Anthropic's is more aggressive on savings but requires developer effort. OpenAI's is zero-config but less generous on discounts.

๐Ÿ“Š

What to Cache (Design Patterns)

Pattern 1: Cache System Prompt + Tools

The most common pattern. Your system prompt and tool definitions are the same across all requests:

python

This prefix is identical across ALL user requests โ†’ perfect for caching

snippet
code
SYSTEM_PROMPT = """

You are an AI assistant for our e-commerce platform.

You can help customers with orders, returns, product info, and account issues.

Rules

...

[2,000 tokens of instructions]

"""

TOOLS = [

example
code
# 20 tool definitions, ~3,000 tokens total
    {"type": "function", "function": {"name": "lookup_order", ...}},
    {"type": "function", "function": {"name": "process_return", ...}},
    ...

]

Total cached prefix: ~5,000 tokens

At 100 requests/day: saves ~$1.50/day with Anthropic

Pattern 2: Cache RAG Context

If multiple users ask about the same document, cache the document content:

python

Cache the document content, vary only the question

snippet
code
messages = [
{
        "role": "system",
        "content": f"Answer questions about this document:\n\n{document_text}",
        "cache_control": {"type": "ephemeral"}  # Anthropic
    },
    {"role": "user", "content": user_question}  # Different per user

]

Pattern 3: Cache Conversation History

In multi-turn conversations, the history grows but the prefix stays the same:

python

Turn 1: [system, user1] โ†’ NEW

Turn 2: [system, user1, assistant1, user2] โ†’ system+user1 CACHED

Turn 3: [system, user1, assistant1, user2, assistant2, user3] โ†’ more CACHED

Each turn re-processes less new content

Pattern 4: Stable Prefix, Variable Suffix

Structure your prompts so the stable parts come first:

python

GOOD โ€” stable prefix, variable suffix

snippet
code
messages = [
{"role": "system", "content": long_stable_instructions},     # Cached โœ…
    {"role": "system", "content": tool_definitions},              # Cached โœ…
    {"role": "system", "content": f"Today's date: {today}"},     # Changes daily โŒ
    {"role": "user", "content": user_message}                     # Changes per request โŒ

]

BAD โ€” variable content mixed into prefix

snippet
code
messages = [
{"role": "system", "content": f"Today is {today}. {long_instructions}"},  # Never cached!
    {"role": "user", "content": user_message}

]

Key insight: Put anything that changes (dates, user info, dynamic context) AFTER the stable prefix. Anything before the first change gets cached; anything after does not.

Measuring Cache Performance

Anthropic

python

snippet
code
usage = response.usage
total_input = usage.input_tokens
cached = usage.cache_read_input_tokens
cache_written = usage.cache_creation_input_tokens
hit_rate = cached / (total_input + cached + cache_written) * 100
print(f"Cache hit rate: {hit_rate:.1f}%")

OpenAI

python

snippet
code
usage = response.usage
cached = usage.prompt_tokens_details.cached_tokens
total = usage.prompt_tokens
hit_rate = cached / total * 100
print(f"Cache hit rate: {hit_rate:.1f}%")

Track these over time. A healthy application should see 60-90% cache hit rates. If it's below 30%, your prompts might have too much variable content in the prefix.

๐Ÿ’ก

Common Mistakes

  1. Putting dynamic content at the start

python

BAD โ€” timestamp breaks cache every second

{"role": "system", "content": f"Current time: {datetime.now()}\n{instructions}"}

GOOD โ€” timestamp at the end

{"role": "system", "content": f"{instructions}\nCurrent time: {datetime.now()}"}

  1. Randomizing prompt order

python

BAD โ€” shuffling tool definitions changes the prefix

random.shuffle(tools)

GOOD โ€” keep tools in consistent order

tools.sort(key=lambda t: t["function"]["name"])

  1. Forgetting cache TTL

If requests are more than 5 minutes apart, the cache expires.

For low-traffic applications, caching won't help much.

  1. Too-short prefixes

Anthropic needs 1,024+ tokens to cache.

A 200-token system prompt won't be cached โ€” pad it with detailed instructions.

๐Ÿ”ฌ

Practical Takeaways

Put stable content first โ€” system prompts, tool definitions, reference docs โ†’ then dynamic content

Anthropic: explicit control with 90% savings โ€” use cache_control for maximum benefit

OpenAI: automatic with 50% savings โ€” zero effort, just design your prompt well

Cache breaks on first differing byte โ€” keep prefixes perfectly consistent

First call costs slightly more (Anthropic) โ€” but pays back by the 3rd call

Track cache hit rates โ€” aim for 60-90%, investigate if below 30%

๐Ÿ›ก๏ธ

What's Next?

snippet
code
Episode 39: Rate Limiting & Token Budgets โ€” You've optimized costs with caching. Now you need to survive rate limits. TPM, RPM, 429 errors, retry strategies, and how to build production systems that don't crash when they hit the wall.

โ† Previous

Ep 37: Streaming

Next โ†’

Ep 39: Rate Limiting & Token Budgets

Next: Episode 39 โ€” Rate Limiting & Token Budgets

Ten users? Flawless. A hundred? Smooth. Two thousand hit simultaneously? HTTP 429. Welcome to rate limiting.

This is part of a 98-episode series covering AI engineering from tokens to production deployment.

โ† Previous Ep 37: Streaming: Why AI Responds One Word at a Time (And Why โ€ฆ