HelloAI
L4 Chapter 15 🐣 🕒 11 min

Prompt Caching in Production: Save 90% of LLM Cost

Prefix caching is supported by Anthropic / OpenAI / Gemini — stop re-paying for the same system prompt every request.

A
Alai
9/20/2026

In LLM cost structure, input tokens are usually the largest component — especially with big system prompts, RAG retrieval, or document analysis.

Prompt Caching is the most important cost optimization of the past two years:

Charge the same prefix once; subsequent hits drop to ~1/10 of the price.

Who pays this tax

Typical scenario:

Your system prompt = 5000 tokens
Each user request = 100 tokens of user input + 800 tokens output

Each billed call = 5000 + 100 input + 800 output

But the system prompt never changes —
why re-pay for it every time?

10,000 requests per day:

  • Without cache: 5000 × 10000 = 50M input tokens
  • With cache: 5000 once + 100 × 10000 = 6M input tokenssave 88%

Prompt Caching across three vendors

Anthropic (most explicit + most practical)

response = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    system=[
        {"type": "text", "text": "Short instruction"},
        {
            "type": "text",
            "text": LARGE_KNOWLEDGE_BASE,  # large chunk
            "cache_control": {"type": "ephemeral"}  # ← key
        }
    ],
    messages=[{"role": "user", "content": user_question}]
)

Effect:

  • Cache write: 1.25× normal input price (first time)
  • Cache read: 0.1× normal price = save 90%
  • Cache TTL: 5 minutes (ephemeral); newer SKUs offer 1 hour

Up to 4 cache_control breakpoints in a single request.

OpenAI (automatic)

OpenAI has no explicit cache_control — it automatically detects repeated prefixes (>= 1024 tokens) and caches them:

# No special config needed
response = openai.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": LARGE_SYSTEM_PROMPT},
        {"role": "user", "content": user_q}
    ]
)
# Second identical request → automatic cache hit → 50% discount

Effect:

  • Automatic detection, no code changes
  • Cache hit: 50% discount (less aggressive than Anthropic’s 90%)
  • Cache TTL: ~5-10 minutes

Gemini (context caching)

Requires explicit creation:

from google.generativeai import caching

cache = caching.CachedContent.create(
    model="gemini-2.5-pro",
    system_instruction=LARGE_SYSTEM_PROMPT,
    contents=[],
    ttl=datetime.timedelta(hours=1)
)

response = model.generate_content(
    "user question",
    cached_content=cache.name
)

Effect:

  • Cache read: ~75% discount
  • TTL controllable (up to several hours)
  • Good for long-document RAG

The alignment principle

All vendors share a low-level logic:

Caching matches by token prefix — the prefix must be byte-identical.

Meaning:

Request A:
  system: "You are an assistant. Knowledge base: [10k tokens]"
  user:   "What is X?"

Request B:
  system: "You are an assistant. Knowledge base: [10k tokens]"
  user:   "What is Y?"

                10k tokens identical → cache hit → save money

Request C:
  system: "You are a helpful assistant. Knowledge base: [10k tokens]"

                One word different → no hit → full price

This is why you must be careful with variable content — dates, random IDs, usernames at the front break the cache.

A real-world template

def build_request(user_query: str, user_context: dict):
    return {
        "system": [
            # ① Cache layer 1: invariant instructions
            {"type": "text", "text": SYSTEM_INSTRUCTIONS,
             "cache_control": {"type": "ephemeral"}},

            # ② Cache layer 2: large knowledge base (invariant)
            {"type": "text", "text": KNOWLEDGE_BASE_10K,
             "cache_control": {"type": "ephemeral"}},

            # ③ NOT cached: per-request user context
            {"type": "text", "text": f"User profile: {json.dumps(user_context)}"},
        ],
        "messages": [{"role": "user", "content": user_query}]
    }

Order matters: variable content must come after the invariant prefix. Putting variables first invalidates every cache.

Common RAG mistake

# ❌ Wrong: retrieval first, question after
prompt = f"""
{retrieved_chunks}        ← Different each question
SYSTEM_INSTRUCTIONS
User: {user_query}
"""

# ✓ Right: invariant parts first
prompt = f"""
SYSTEM_INSTRUCTIONS       ← Cache this
RETRIEVAL_PROMPT_TEMPLATE
{retrieved_chunks}        ← Variable, not cached
User: {user_query}
"""

Can’t guarantee cache hits — but maximize hit rate.

Cache blind spots

1. ❌ Streaming responses may not show cache hits immediately

At the start of streaming, cache info may not be propagated yet. Read total token usage from the completed response.

2. ❌ Tool / function calling changes the prefix

When your tools list changes, the prefix changes too — don’t modify tools frequently.

3. ❌ Conversation memory breaks caches easily

# Multi-turn — full history sent each time
messages = [
    {"role": "system", "content": ...},   # cached
    {"role": "user", "content": "Q1"},
    {"role": "assistant", "content": "A1"},  # history
    {"role": "user", "content": "Q2"},
    {"role": "assistant", "content": "A2"},  # history
    {"role": "user", "content": "Q3"},   # new
]

Each new turn changes the prefix! Place cache_control immediately after the system segment, then let history grow.

Monitoring

OpenAI / Anthropic return cache usage in the response:

# Anthropic
print(response.usage)
# cache_creation_input_tokens: 5000  ← wrote 5000 tokens to cache
# cache_read_input_tokens: 0          ← 0 hits

# Second identical request
# cache_creation_input_tokens: 0
# cache_read_input_tokens: 5000       ← hit!

Log these two metrics and compute monthly savings:

def estimated_savings(cache_read_tokens, model="claude-sonnet"):
    # 90% off on cache reads
    full_price_per_M = 3.00  # input
    cached_price_per_M = 0.30
    saved_per_M = full_price_per_M - cached_price_per_M
    return cache_read_tokens / 1_000_000 * saved_per_M

Anti-patterns

Anti-patternConsequence
Putting current timestamp in system promptCache invalidates every request
Putting user ID in the system prefixPer-user cache = no shared savings
Frequently changing tool listsCache frequently invalidates
< 1024 token prompt expecting cacheOpenAI won’t cache
Splitting into too many breakpointsAnthropic limits to 4

Pair with Semantic Cache

Prompt Caching saves when the prefix is byte-identical. Semantic Cache returns the previous answer when the question is semantically similar:

User A: "What's the capital of France?"  →  call LLM, cache result
User B: "France capital?"                 →  embedding match → return A's answer

The two are complementary — Prompt Caching saves input tokens; Semantic Cache skips the LLM call entirely.

Estimated savings

At 2026 pricing, 5000-token system prompt + 10k requests/day:

StateMonthly input cost
No cache$4,500
OpenAI auto 50% off$2,250
Anthropic 90% off$450

A simple optimization, thousands of dollars saved per month.

💡 A principle

Prompt Caching is the most underrated LLM optimization of 2024-2025:

  • Setup cost: < 1 hour
  • Savings: 50-90%
  • Applies to ~80% of apps

Any product using LLMs in production should implement prompt caching within the first week. Not doing it = paying thousands of dollars a month for nothing.

Next: L4-12 LLM Cost Optimization or L7-03 Inference Optimization.

🚧 3 Common Pitfalls

⚠️ Real-world traps

Pitfall 1: Assuming caches hit automatically You need explicit cache breakpoints (Anthropic) / matching prefixes (OpenAI) — without that you’re not using caching.

Pitfall 2: TTL too short for your traffic pattern Default 5-10 min, each request refreshes — high-traffic apps stay warm, low-traffic apps frequently miss.

Pitfall 3: Dynamic content inside the cached segment Cached segments must have a stable prefix — user query / timestamp / random ID inside the cached prompt = 100% miss.