🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

LLM Prompt Caching: How to Cut Your Claude API Costs by 80%

How to use Claude API's prompt caching feature to dramatically reduce costs and latency for LLM applications with repeated context — system prompts, documents, conversation history, and RAG results.

Shubham6 min read
Share:Tweet

If you are building production LLM applications and not using prompt caching, you are probably paying 5-10x more than you need to.

Prompt caching lets you cache the expensive input tokens — your system prompt, background documents, conversation history — and reuse them across requests without re-processing. For applications with large, repeated context, this is the single biggest cost lever available.

Here is how to use it properly with Claude API.

How Prompt Caching Works

Normally, every API call processes all input tokens from scratch. If your system prompt is 2,000 tokens and you make 10,000 API calls per day, you are processing 20 million tokens just for the system prompt — every single day.

With prompt caching, Claude processes and stores the prompt up to a cache breakpoint. Subsequent requests that share the same prefix hit the cache and pay a fraction of the normal input token price.

The economics (Claude Sonnet 5):

  • Normal input: $3.00 per million tokens
  • Cache write: $3.75 per million tokens (first time, 25% more)
  • Cache read: $0.30 per million tokens (10x cheaper)

So if your 2,000 token system prompt is called 10,000 times per day:

  • Without caching: 20M tokens × $3.00 = $60/day
  • With caching: 2K tokens × $3.75 (write once) + 19,998K × $0.30 = $6/day

That is 90% cost reduction on that component alone.

Setting Up Prompt Caching

You enable caching by adding cache_control with type: "ephemeral" to content blocks you want cached.

python
import anthropic
 
client = anthropic.Anthropic()
 
 
def create_devops_assistant_response(user_question: str) -> str:
    """
    DevOps assistant with cached system context.
    The large system prompt is cached — only processed once per 5-minute window.
    """
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        system=[
            {
                "type": "text",
                "text": """You are an expert DevOps engineer assistant specializing in:
- Kubernetes (CKA-level knowledge, troubleshooting, production patterns)
- AWS (EKS, EC2, RDS, IAM, networking, cost optimization)
- Terraform (modules, state management, best practices)
- CI/CD (GitHub Actions, ArgoCD, GitOps patterns)
- Observability (Prometheus, Grafana, Loki, OpenTelemetry)
- Security (RBAC, network policies, secrets management, DevSecOps)
 
When answering:
1. Give specific, actionable answers with exact commands
2. Reference real-world patterns from production environments
3. Point out common pitfalls when relevant
4. Use concrete examples over abstract explanations
 
[... your full 2000+ token system prompt here ...]""",
                "cache_control": {"type": "ephemeral"}  # Cache this block
            }
        ],
        messages=[
            {"role": "user", "content": user_question}
        ]
    )
 
    # Check cache status in response
    usage = response.usage
    print(f"Input tokens: {usage.input_tokens}")
    print(f"Cache read tokens: {getattr(usage, 'cache_read_input_tokens', 0)}")
    print(f"Cache write tokens: {getattr(usage, 'cache_creation_input_tokens', 0)}")
 
    return response.content[0].text

The first call writes to cache (slightly more expensive). Subsequent calls within the 5-minute TTL read from cache at 10x cheaper rate.

Caching Conversation History

For multi-turn conversations, cache everything except the latest user message:

python
def multi_turn_with_caching(
    conversation_history: list[dict],
    new_user_message: str,
    system_prompt: str
) -> tuple[str, list[dict]]:
    """
    Handle multi-turn conversation with caching on history.
    Caches all previous messages so only new ones are processed fresh.
    """
    # Build messages with cache on all but last turn
    messages = []
 
    # Add conversation history with cache breakpoint at the end
    if conversation_history:
        for i, msg in enumerate(conversation_history):
            if i == len(conversation_history) - 1:
                # Cache breakpoint after last history message
                messages.append({
                    "role": msg["role"],
                    "content": [
                        {
                            "type": "text",
                            "text": msg["content"],
                            "cache_control": {"type": "ephemeral"}
                        }
                    ]
                })
            else:
                messages.append(msg)
 
    # Add new user message (not cached — it changes every turn)
    messages.append({"role": "user", "content": new_user_message})
 
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        system=[{
            "type": "text",
            "text": system_prompt,
            "cache_control": {"type": "ephemeral"}
        }],
        messages=messages
    )
 
    assistant_reply = response.content[0].text
 
    # Update history
    updated_history = conversation_history + [
        {"role": "user", "content": new_user_message},
        {"role": "assistant", "content": assistant_reply}
    ]
 
    return assistant_reply, updated_history

Caching RAG Documents

This is where prompt caching delivers the most dramatic savings. When you retrieve documents for RAG and include them in the prompt, caching the retrieved documents means you only process them once even across multiple questions about the same context.

python
def rag_with_cached_context(
    retrieved_docs: list[str],
    user_question: str
) -> str:
    """
    RAG query where retrieved documents are cached.
    Multiple questions about the same docs only process them once.
    """
    context = "\n\n---\n\n".join(retrieved_docs)
 
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        system=[
            {
                "type": "text",
                "text": "Answer questions based on the provided documentation context.",
                "cache_control": {"type": "ephemeral"}
            },
            {
                "type": "text",
                "text": f"## Documentation Context\n\n{context}",
                "cache_control": {"type": "ephemeral"}  # Cache the retrieved docs
            }
        ],
        messages=[
            {"role": "user", "content": user_question}  # Only question changes
        ]
    )
 
    return response.content[0].text
 
 
# Example: Multiple questions about the same runbook
runbook_docs = [load_runbook("payment-api"), load_runbook("postgres")]
 
# First question — writes to cache
answer1 = rag_with_cached_context(runbook_docs, "How do I restart the payment service?")
 
# Second question — reads from cache (10x cheaper)
answer2 = rag_with_cached_context(runbook_docs, "What are the postgres backup procedures?")

Cache TTL and When It Expires

Claude's cache has a 5-minute TTL (time to live). Each cache hit resets the TTL — so actively used caches stay warm indefinitely, while unused ones expire after 5 minutes of inactivity.

This means:

  • For high-traffic applications (many requests per minute), caches stay warm and you always get cache hits
  • For low-traffic applications (requests every 10+ minutes), you will frequently pay cache write costs

Design implication: If your application has bursty traffic, the 5-minute window covers burst periods well. For very low traffic, consider whether the caching economics make sense — sometimes the cache write overhead exceeds the savings.

Minimum Cache Size

Not all content is worth caching. Claude only caches content above a minimum token threshold:

  • Claude Sonnet / Opus: minimum 1,024 tokens to cache
  • Claude Haiku: minimum 2,048 tokens to cache

If your system prompt is 500 tokens, it will not be cached even with cache_control set. In this case, expand your system prompt with more detailed instructions or combine it with other context to exceed the minimum.

Tracking Cache Performance

Always log cache metrics in production:

python
import logging
 
logger = logging.getLogger(__name__)
 
 
def call_with_cache_metrics(messages: list, system: list, model: str = "claude-sonnet-5") -> str:
    response = client.messages.create(
        model=model,
        max_tokens=1024,
        system=system,
        messages=messages
    )
 
    usage = response.usage
    cache_read = getattr(usage, "cache_read_input_tokens", 0)
    cache_write = getattr(usage, "cache_creation_input_tokens", 0)
    regular_input = usage.input_tokens - cache_read - cache_write
 
    cache_hit_rate = cache_read / usage.input_tokens if usage.input_tokens > 0 else 0
 
    logger.info({
        "event": "llm_call",
        "model": model,
        "regular_input_tokens": regular_input,
        "cache_read_tokens": cache_read,
        "cache_write_tokens": cache_write,
        "cache_hit_rate": f"{cache_hit_rate:.1%}",
        "output_tokens": usage.output_tokens,
    })
 
    return response.content[0].text

Target cache hit rates:

  • > 70% — caching is working well, significant cost savings
  • 30-70% — reasonable, check if traffic patterns match TTL window
  • < 30% — either context changes too frequently, traffic is too low, or TTL mismatches

Quick Reference: When to Cache What

ContentCache?Reason
System prompt (>1024 tokens)AlwaysSame across all requests
Retrieved documents (RAG)YesSame docs, multiple questions
Conversation historyYesGrows but stays stable within session
User's current messageNeverChanges every request
Dynamic data (current time, live metrics)NeverChanges constantly

Prompt caching is one of the highest-leverage optimizations available for production LLM applications. If you are not using it, add it today — the implementation is minimal and the cost reduction is immediate.


More LLMOps content? Read our LLM context window management guide and LLM evaluation with LLM-as-judge.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments