Advanced Prompt Caching Patterns for Claude Code
Overview
Prompt caching is a critical optimization technique that can reduce Claude API costs by up to 90% and latency by up to 85%. This guide covers advanced implementation patterns, cost analysis, and real-world examples for maximizing the efficiency of Claude Code applications.
Key Benefits
- 90% Cost Reduction: Cache reads cost 3.00/MTok for standard input
- 85% Latency Reduction: Skip redundant processing for cached content
- Improved Throughput: Cache read tokens don’t count against rate limits (Claude 3.7 Sonnet)
- Better User Experience: Faster responses for repetitive queries
Pricing Structure (2025)
Base Pricing
- Claude 4 Opus: 75/MTok output
- Claude 4 Sonnet: 15.00/MTok output
- Claude 3.5 Haiku: 4.00/MTok output
Cache Pricing
- Cache Write: $3.75/MTok (25% premium over base)
- Cache Read: $0.30/MTok (90% discount)
- Break-even: 2-3 uses of cached content
Implementation Patterns
1. Basic Python Implementation
import anthropic
from typing import List, Dict, Any
import time
class CachedClaudeClient:
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
self.cache_metrics = {
"cache_writes": 0,
"cache_reads": 0,
"total_saved": 0.0
}
def create_cached_message(
self,
system_prompt: str,
context: str,
user_query: str,
model: str = "claude-3-5-sonnet-20241022"
) -> Dict[str, Any]:
"""
Create a message with intelligent caching of system prompt and context.
"""
start_time = time.time()
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"} # Cache system instructions
},
{
"type": "text",
"text": context,
"cache_control": {"type": "ephemeral"} # Cache large context
},
{
"type": "text",
"text": user_query # Don't cache variable user input
}
]
}
]
response = self.client.messages.create(
model=model,
messages=messages,
max_tokens=1000
)
# Track metrics
self._update_metrics(response)
return {
"response": response,
"latency": time.time() - start_time,
"cache_metrics": self.cache_metrics
}
def _update_metrics(self, response):
"""Update cache metrics based on response."""
if hasattr(response, 'usage'):
if response.usage.cache_creation_input_tokens > 0:
self.cache_metrics["cache_writes"] += 1
if response.usage.cache_read_input_tokens > 0:
self.cache_metrics["cache_reads"] += 1
# Calculate savings (90% discount on cached tokens)
tokens_saved = response.usage.cache_read_input_tokens
cost_saved = (tokens_saved / 1_000_000) * 2.70 # $3.00 - $0.30
self.cache_metrics["total_saved"] += cost_saved2. TypeScript Implementation with Monitoring
import Anthropic from '@anthropic-ai/sdk';
interface CacheMetrics {
cacheWrites: number;
cacheReads: number;
totalSaved: number;
avgLatency: number;
}
interface CacheControl {
type: 'ephemeral';
ttl?: number; // Optional TTL in seconds
}
class CachedClaudeClient {
private client: Anthropic;
private metrics: CacheMetrics = {
cacheWrites: 0,
cacheReads: 0,
totalSaved: 0,
avgLatency: 0
};
private latencies: number[] = [];
constructor(apiKey: string) {
this.client = new Anthropic({ apiKey });
}
async createCachedMessage(
systemPrompt: string,
context: string,
userQuery: string,
model: string = 'claude-3-5-sonnet-20241022'
) {
const startTime = Date.now();
const message = await this.client.messages.create({
model,
messages: [{
role: 'user',
content: [
{
type: 'text',
text: systemPrompt,
cache_control: { type: 'ephemeral' }
},
{
type: 'text',
text: context,
cache_control: { type: 'ephemeral' }
},
{
type: 'text',
text: userQuery
}
]
}],
max_tokens: 1000
});
const latency = Date.now() - startTime;
this.updateMetrics(message, latency);
return {
response: message,
latency,
metrics: this.getMetrics()
};
}
private updateMetrics(response: any, latency: number) {
this.latencies.push(latency);
this.metrics.avgLatency =
this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length;
if (response.usage?.cache_creation_input_tokens > 0) {
this.metrics.cacheWrites++;
}
if (response.usage?.cache_read_input_tokens > 0) {
this.metrics.cacheReads++;
const tokensSaved = response.usage.cache_read_input_tokens;
const costSaved = (tokensSaved / 1_000_000) * 2.70;
this.metrics.totalSaved += costSaved;
}
}
getMetrics(): CacheMetrics {
return { ...this.metrics };
}
}3. Multi-Agent Cost Optimization Pattern
from dataclasses import dataclass
from typing import Optional, List
import hashlib
@dataclass
class AgentContext:
"""Shared context for multi-agent systems."""
system_instructions: str
shared_knowledge: str
task_history: List[str]
def get_cache_key(self) -> str:
"""Generate a unique cache key for this context."""
content = f"{self.system_instructions}:{self.shared_knowledge}"
return hashlib.md5(content.encode()).hexdigest()
class MultiAgentCacheManager:
"""Manages caching across multiple Claude agents."""
def __init__(self, client: anthropic.Anthropic):
self.client = client
self.context_cache = {}
self.cache_usage = {}
def register_agent_context(self, agent_id: str, context: AgentContext):
"""Register shared context for an agent."""
cache_key = context.get_cache_key()
self.context_cache[agent_id] = {
"key": cache_key,
"context": context,
"last_used": time.time()
}
def create_agent_message(
self,
agent_id: str,
query: str,
model: str = "claude-3-5-sonnet-20241022"
):
"""Create message with cached agent context."""
if agent_id not in self.context_cache:
raise ValueError(f"Agent {agent_id} not registered")
context_info = self.context_cache[agent_id]
context = context_info["context"]
# Build message with cached components
messages = [{
"role": "user",
"content": [
{
"type": "text",
"text": context.system_instructions,
"cache_control": {"type": "ephemeral"}
},
{
"type": "text",
"text": context.shared_knowledge,
"cache_control": {"type": "ephemeral"}
},
{
"type": "text",
"text": f"Task History:\n" + "\n".join(context.task_history[-5:])
},
{
"type": "text",
"text": query
}
]
}]
response = self.client.messages.create(
model=model,
messages=messages,
max_tokens=2000
)
# Update usage tracking
context_info["last_used"] = time.time()
self._track_usage(agent_id, response)
return response
def _track_usage(self, agent_id: str, response):
"""Track cache usage per agent."""
if agent_id not in self.cache_usage:
self.cache_usage[agent_id] = {
"total_requests": 0,
"cache_hits": 0,
"tokens_saved": 0,
"cost_saved": 0.0
}
usage = self.cache_usage[agent_id]
usage["total_requests"] += 1
if hasattr(response, 'usage') and response.usage.cache_read_input_tokens > 0:
usage["cache_hits"] += 1
usage["tokens_saved"] += response.usage.cache_read_input_tokens
usage["cost_saved"] += (response.usage.cache_read_input_tokens / 1_000_000) * 2.70
def get_savings_report(self) -> Dict[str, Any]:
"""Generate cost savings report across all agents."""
total_saved = sum(u["cost_saved"] for u in self.cache_usage.values())
total_requests = sum(u["total_requests"] for u in self.cache_usage.values())
total_hits = sum(u["cache_hits"] for u in self.cache_usage.values())
return {
"total_cost_saved": f"${total_saved:.2f}",
"cache_hit_rate": f"{(total_hits/total_requests*100):.1f}%" if total_requests > 0 else "0%",
"per_agent_savings": {
agent_id: {
"cost_saved": f"${usage['cost_saved']:.2f}",
"hit_rate": f"{(usage['cache_hits']/usage['total_requests']*100):.1f}%"
}
for agent_id, usage in self.cache_usage.items()
}
}4. Intelligent Cache Invalidation Pattern
interface CacheEntry {
content: string;
checksum: string;
created: number;
lastUsed: number;
useCount: number;
ttl: number;
}
class SmartCacheManager {
private cache: Map<string, CacheEntry> = new Map();
private readonly DEFAULT_TTL = 300; // 5 minutes
private readonly EXTENDED_TTL = 3600; // 1 hour
constructor(private client: Anthropic) {
// Start cleanup interval
setInterval(() => this.cleanup(), 60000); // Every minute
}
async cachedRequest(
key: string,
content: string,
userQuery: string,
options: {
ttl?: number;
autoExtend?: boolean;
} = {}
) {
const checksum = this.generateChecksum(content);
const existingEntry = this.cache.get(key);
// Check if content has changed (invalidate if so)
if (existingEntry && existingEntry.checksum !== checksum) {
console.log(`Cache invalidated for key: ${key} - content changed`);
this.cache.delete(key);
}
// Determine if we should use cache
const shouldCache = this.shouldCache(content);
const ttl = options.ttl || this.DEFAULT_TTL;
const messages = [{
role: 'user' as const,
content: [
{
type: 'text' as const,
text: content,
...(shouldCache && { cache_control: { type: 'ephemeral' as const } })
},
{
type: 'text' as const,
text: userQuery
}
]
}];
const response = await this.client.messages.create({
model: 'claude-3-5-sonnet-20241022',
messages,
max_tokens: 2000
});
// Update cache entry
if (shouldCache) {
const entry = this.cache.get(key) || {
content,
checksum,
created: Date.now(),
lastUsed: Date.now(),
useCount: 0,
ttl
};
entry.lastUsed = Date.now();
entry.useCount++;
// Auto-extend TTL for frequently used entries
if (options.autoExtend && entry.useCount > 5) {
entry.ttl = this.EXTENDED_TTL;
}
this.cache.set(key, entry);
}
return response;
}
private shouldCache(content: string): boolean {
// Only cache content > 1024 tokens (rough estimate)
return content.length > 4096; // ~1024 tokens
}
private generateChecksum(content: string): string {
// Simple checksum for demo - use crypto.subtle in production
return content.split('').reduce((a, b) => {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
}, 0).toString();
}
private cleanup() {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.lastUsed > entry.ttl * 1000) {
this.cache.delete(key);
console.log(`Cache expired for key: ${key}`);
}
}
}
getCacheStats() {
const entries = Array.from(this.cache.values());
const totalUses = entries.reduce((sum, e) => sum + e.useCount, 0);
const avgUseCount = entries.length > 0 ? totalUses / entries.length : 0;
return {
entries: entries.length,
totalUses,
avgUseCount: avgUseCount.toFixed(2),
memoryUsage: entries.reduce((sum, e) => sum + e.content.length, 0)
};
}
}Best Practices
1. Structure Prompts for Caching
# GOOD: Static content first, variable content last
messages = [
{
"role": "user",
"content": [
# Cache large, stable content
{"type": "text", "text": system_prompt, "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": documentation, "cache_control": {"type": "ephemeral"}},
# Don't cache variable content
{"type": "text", "text": user_query}
]
}
]
# BAD: Mixed static and variable content
messages = [
{
"role": "user",
"content": system_prompt + user_query # Can't cache effectively
}
]2. Monitor Cache Performance
def analyze_cache_efficiency(metrics: Dict[str, Any]) -> Dict[str, Any]:
"""Analyze cache performance and suggest optimizations."""
hit_rate = metrics["cache_hits"] / metrics["total_requests"]
avg_savings_per_request = metrics["total_saved"] / metrics["total_requests"]
recommendations = []
if hit_rate < 0.5:
recommendations.append("Consider caching more stable content")
if avg_savings_per_request < 0.01: # Less than $0.01 saved per request
recommendations.append("Increase size of cached content")
return {
"hit_rate": f"{hit_rate * 100:.1f}%",
"avg_savings": f"${avg_savings_per_request:.3f}",
"recommendations": recommendations
}3. Cache Warming Strategy
async def warm_cache(client: CachedClaudeClient, contexts: List[Dict[str, str]]):
"""Pre-warm cache with common contexts."""
for context in contexts:
try:
await client.create_cached_message(
system_prompt=context["system"],
context=context["data"],
user_query="Cache warming - please acknowledge.",
model="claude-3-5-haiku-20241022" # Use cheaper model for warming
)
print(f"✓ Warmed cache for: {context['name']}")
except Exception as e:
print(f"✗ Failed to warm cache for {context['name']}: {e}")Cost Analysis Example
def calculate_roi(
requests_per_day: int,
avg_input_tokens: int,
cache_hit_rate: float = 0.8,
days: int = 30
) -> Dict[str, float]:
"""Calculate ROI for implementing prompt caching."""
# Costs per million tokens
STANDARD_COST = 3.00 # $3.00 per MTok
CACHE_WRITE_COST = 3.75 # $3.75 per MTok (first write)
CACHE_READ_COST = 0.30 # $0.30 per MTok (subsequent reads)
total_requests = requests_per_day * days
tokens_per_request = avg_input_tokens / 1_000_000
# Without caching
cost_without_cache = total_requests * tokens_per_request * STANDARD_COST
# With caching
cache_writes = requests_per_day # Assume daily cache refresh
cache_reads = total_requests * cache_hit_rate
uncached_requests = total_requests * (1 - cache_hit_rate)
cost_with_cache = (
(cache_writes * tokens_per_request * CACHE_WRITE_COST) +
(cache_reads * tokens_per_request * CACHE_READ_COST) +
(uncached_requests * tokens_per_request * STANDARD_COST)
)
savings = cost_without_cache - cost_with_cache
roi_percentage = (savings / cost_without_cache) * 100
return {
"cost_without_cache": cost_without_cache,
"cost_with_cache": cost_with_cache,
"total_savings": savings,
"roi_percentage": roi_percentage,
"break_even_days": (cache_writes * tokens_per_request *
(CACHE_WRITE_COST - STANDARD_COST)) /
(savings / days) if savings > 0 else float('inf')
}
# Example calculation
roi = calculate_roi(
requests_per_day=1000,
avg_input_tokens=5000,
cache_hit_rate=0.8
)
print(f"Monthly savings: ${roi['total_savings']:.2f}")
print(f"ROI: {roi['roi_percentage']:.1f}%")
print(f"Break-even: {roi['break_even_days']:.1f} days")Common Pitfalls
1. Over-Caching
- Don’t cache small prompts (< 1024 tokens)
- Avoid caching rapidly changing content
- Monitor cache miss rates
2. Cache Key Collisions
- Use content hashing for cache keys
- Include version identifiers
- Implement proper invalidation
3. Memory Management
- Implement cache size limits
- Use LRU eviction policies
- Monitor memory usage
Conclusion
Prompt caching is essential for cost-effective Claude Code applications. With proper implementation, you can achieve:
- 90% cost reduction on repeated queries
- 85% latency improvement
- Better scalability for multi-agent systems
- Improved user experience
Start with basic caching for system prompts and gradually implement more sophisticated patterns based on your usage patterns and requirements.