Claude API Prompt Caching Tutorial 2026: Cut Your API Costs by Up to 90%
ai-productivity

Claude API Prompt Caching Tutorial 2026: Cut Your API Costs by Up to 90%

Ricardo Gil
September 16, 2026
15 min read
#claude-api #prompt-caching #api-cost-optimization #developer-tutorial #anthropic
πŸ›’

Products in This Post

Affiliate links

As an Amazon Associate I earn from qualifying purchases at no extra cost to you.

Why I Started Caring About Prompt Caching

When I first wired Claude API calls into my n8n automation workflows, my token costs climbed faster than I expected. Every time a workflow fired, I was resending the same 3,500-token system prompt β€” the one encoding my code review rules, my .NET coding standards, my Angular style preferences, and a handful of hard-won team conventions. Multiply that by 40 or 50 automation runs per day and you're paying full price on tokens that carry zero new information.

Prompt caching changed this completely. It's been available in the Anthropic API since mid-2024, and by 2026 it's mature enough that I consider it a required pattern for any production integration I build. The core idea is simple: mark a block of your prompt with a cache_control header, and if the same content appears at the start of a subsequent request within a 5-minute window, Anthropic serves it from cache at roughly 10% of the original read cost. Cache hits carry zero additional latency.

On my code review workflow specifically, that's an 87% reduction on the system prompt portion of my API bill β€” taking that Claude API line item from about $38/month down to $5. This tutorial covers exactly how I set that up: the underlying mechanics, working code in Python and Node.js, my real n8n integration pattern, and the gotchas that cost me time before I sorted them out.

What Prompt Caching Actually Does (The Non-Fluffy Version)

Anthropic's prompt caching works at the prefix level. When you tag a content block with "cache_control": {"type": "ephemeral"}, the API stores a snapshot of the KV cache state for that prefix. On subsequent requests where that exact prefix appears β€” same content, same byte-for-byte sequence, same position in the prompt β€” the model skips reprocessing it entirely and loads the cached state. The result lands in your response's usage.cache_read_input_tokens field.

The cache TTL is 5 minutes by default for ephemeral caches, and each cache hit refreshes that timer. So if you're running an automation that fires every couple of minutes with the same large system prompt β€” like my Forgejo webhook integration β€” you'll maintain a warm cache continuously and only pay the write cost once per idle period. That write cost is 25% more expensive than a standard input token, but you recoup that immediately on the second request.

To make the math concrete: as of September 2026, on Claude Sonnet 4.5, you're looking at cache writes costing 1.25Γ— a standard input token (one-time, to populate the cache) and cache reads costing 0.10Γ— the standard rate. Say you have a 3,000-token system prompt running 50 times per hour. Without caching: 50 Γ— 3,000 = 150,000 input tokens per hour at full price. With caching: one write of 3,750 token-equivalents, plus 49 reads at 300 token-equivalents each β€” roughly 18,450 effective units. That's an 87.7% reduction on the cached portion alone.

One constraint that matters: the minimum cacheable block is 1,024 tokens. Below this threshold, the API silently ignores your cache_control directive and processes the tokens normally. You won't get an error; you just won't see cache reads in your usage output. I'll come back to this in the pitfalls section, because it's the most common mistake I see from developers who reach out after reading my earlier posts on Claude API tool use.

When to Use Prompt Caching (And When Not To)

I reach for prompt caching whenever I have a static content block over 1,024 tokens that I'm going to send repeatedly. That includes: long system prompts with coding standards, personas, or domain rules; large reference documents injected into context like API specs, database schemas, or legal boilerplate; multi-turn conversation setups where the same context heads every exchange; and n8n AI Agent nodes that run the same system configuration dozens of times per day.

I skip caching for prompts under the 1,024-token minimum β€” the overhead of thinking about it isn't worth it. I also skip it for one-off analytical queries where I'll never hit the same prefix twice, and for highly dynamic prompts where the "stable" section is less than 30–40% of the total length, since the math doesn't favor caching when you're only occasionally reusing a small fraction of the context.

A quick way to estimate whether caching makes sense: divide your system prompt's character count by 4 to get a rough token estimate. A 4,000-character system prompt is around 1,000 tokens β€” just over the threshold. If you're regularly sending 2,000+ character system prompts and running the workflow more than a few times per day, caching will pay for itself almost immediately. For smaller prompts, the Anthropic SDK's client.messages.count_tokens() method gives you an exact count before you commit to the architecture.

Your First Cached API Request

Let me show you the actual code. This is the Python pattern I use for standalone scripts and testing:

python
import anthropic

client = anthropic.Anthropic()  # uses ANTHROPIC_API_KEY from env

# This system prompt should be 1,024+ tokens to benefit from caching.
# At roughly 4 chars/token, that's ~4,000+ characters.
SYSTEM_PROMPT = """
You are an expert code reviewer specializing in .NET (C# 12, ASP.NET Core 9)
and Angular 20 applications. Apply these standards to every review:

CSHARP STANDARDS:
- Prefer primary constructors for simple dependency injection
- Use required members and init-only setters for immutable DTOs
- Avoid nested ternaries; extract to named methods when logic exceeds one line
- All async methods must propagate CancellationToken; never use .Result or .Wait()
- Use ILogger only, never Console.WriteLine or Debug.Write in production paths
- Entity Framework: always use AsNoTracking() for read-only queries
- Use record types for value objects and DTOs, not class with [Immutable] attributes

ANGULAR STANDARDS:
- Signals-first: use signal(), computed(), and effect() over RxJS where appropriate
- Standalone components only; no NgModule declarations
- Use inject() function over constructor injection for cleaner templates
- OnPush change detection is mandatory for all components
- Never subscribe in templates; use async pipe or toSignal()
- HTTP calls belong in services, never in components or directives

REVIEW OUTPUT FORMAT:
Return findings grouped by: CRITICAL (must fix), MAJOR (should fix), MINOR (consider).
For each finding include: line reference, issue description, corrected code snippet.
If no issues found in a category, write "None."
""" * 3  # Repeat to ensure we exceed 1,024 tokens for this example

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=2048,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT,
            "cache_control": {"type": "ephemeral"}  # The magic line
        }
    ],
    messages=[
        {
            "role": "user",
            "content": "Review this C# service method:\n\n```csharp\npublic async Task> GetOrdersAsync(int userId)\n{\n    return await _context.Orders\n        .Where(o => o.UserId == userId)\n        .ToListAsync();\n}\n```"
        }
    ]
)

# Check the usage object to verify caching
usage = response.usage
print(f"Input tokens: {usage.input_tokens}")
print(f"Cache write tokens: {getattr(usage, 'cache_creation_input_tokens', 0)}")
print(f"Cache read tokens: {getattr(usage, 'cache_read_input_tokens', 0)}")
print(f"Output tokens: {usage.output_tokens}")
print(response.content[0].text)

On the first call, you'll see cache_creation_input_tokens populated. On the second call within 5 minutes with the same system prompt, you'll see cache_read_input_tokens instead β€” that's your cache hit, and those tokens cost a tenth of a standard input token. Here's the TypeScript equivalent I use in Express middleware and serverless functions:

typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // ANTHROPIC_API_KEY from environment

const SYSTEM_PROMPT = `... your 1,024+ token system prompt here ...`;

async function reviewCode(codeSnippet: string): Promise {
  const response = await client.messages.create({
    model: "claude-sonnet-4-5",
    max_tokens: 2048,
    system: [
      {
        type: "text",
        text: SYSTEM_PROMPT,
        // TypeScript SDK uses the same structure
        cache_control: { type: "ephemeral" },
      },
    ],
    messages: [{ role: "user", content: `Review this code:\n\n${codeSnippet}` }],
  });

  // Log cache performance metrics
  const usage = response.usage as any;
  console.log({
    inputTokens: usage.input_tokens,
    cacheWrite: usage.cache_creation_input_tokens ?? 0,
    cacheRead: usage.cache_read_input_tokens ?? 0,
    outputTokens: usage.output_tokens,
    estimatedCostSavings: usage.cache_read_input_tokens
      ? `${((1 - 0.1) * usage.cache_read_input_tokens).toFixed(0)} tokens saved`
      : "cache miss",
  });

  return (response.content[0] as Anthropic.TextBlock).text;
}

Notice the as any cast on the usage object. As of the current SDK version, the cache-specific fields aren't always typed in the base interface even though the API returns them. I flag this in my team's code reviews β€” cast it to a typed interface rather than any in production code. Something like interface CachedUsage extends Anthropic.Usage { cache_creation_input_tokens?: number; cache_read_input_tokens?: number; } does the job cleanly.

Real-World Integration: n8n Workflows with Cached System Prompts

My heaviest use of prompt caching is inside n8n, which I self-host on my Beelink mini PC running Proxmox. The workflow I'm most proud of fires on Forgejo push webhooks, pulls the commit diff via the Forgejo API, and sends it to Claude for review against my coding standards β€” the same standards encoded in that long system prompt above. I built the full n8n + Claude API workflow pattern I reference here, which I covered in detail in my post on n8n + Claude API automation workflows.

The HTTP Request node in n8n gives me full control over the request body, which is what I need for cache_control. I don't use the built-in Anthropic node for caching-sensitive calls. Here's the request body structure I configure in the JSON body field:

json
{
  "model": "claude-sonnet-4-5",
  "max_tokens": 2048,
  "system": [
    {
      "type": "text",
      "text": "{{ $vars.CODE_REVIEW_SYSTEM_PROMPT }}",
      "cache_control": { "type": "ephemeral" }
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": "Review this diff:\n\n{{ $json.diff }}"
    }
  ]
}

I store the system prompt in n8n's built-in variables (Settings β†’ Variables) so it's managed in one place and doesn't clutter the workflow JSON. When I need to update the coding standards, I update the variable β€” but I'm careful about it, because any change to the prompt content invalidates the cache. More on that in the pitfalls section.

The header configuration is straightforward: x-api-key: {{ $credentials.anthropicApi.apiKey }} and Content-Type: application/json. I also add anthropic-version: 2023-06-01 explicitly, which the API requires. Prompt caching doesn't need any special headers beyond what a normal API call requires β€” the cache_control is in the body.

After adding caching, my before/after numbers on that workflow: average 3,400 cached input tokens per run Γ— 50 runs/day = 170,000 tokens/day at full price before. After caching, with a realistic cache hit rate of about 85% (the other 15% are the first call after a 5-minute idle window), I'm effectively paying for 170,000 Γ— (0.15 + 0.85 Γ— 0.10) = 170,000 Γ— 0.235 β‰ˆ 40,000 effective token-cost units per day. That's a 76% reduction β€” more than enough to make the architecture worthwhile.

For the actual hardware running all of this: my Beelink handles n8n, Postgres, Traefik, Forgejo, and a handful of other services without breaking a sweat. If you're building a similar self-hosted automation stack and wondering about hardware, the GMKtec G3 N100 Mini PC (~$189) is the most cost-efficient entry point I've seen β€” N100 with 16GB/1TB handles n8n plus Postgres comfortably. If you want more headroom for heavier workloads or running local Ollama models alongside, the MINISFORUM UM890 Pro with Ryzen 9 8945HS and 32GB (~$429) is what I'd buy today if I were starting fresh β€” it has enough RAM for a 14B Ollama model plus all your services running simultaneously.

Measuring Cache Performance β€” Don't Fly Blind

The Anthropic API always returns cache stats in the usage object. Logging these is the only reliable way to know whether caching is actually working β€” and in my experience, it's not always obvious when it isn't. Here's an example response usage block from a cache hit:

json
{
  "usage": {
    "input_tokens": 47,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 3421,
    "output_tokens": 312
  }
}

On a cache miss (first call, or cache expired), you'd see cache_creation_input_tokens: 3421 and cache_read_input_tokens: 0. The 47 input tokens are the dynamic portion β€” the user message content β€” which is never cached.

I pipe these metrics into a simple Postgres table via another n8n workflow that runs nightly. The schema is just (workflow_id, run_timestamp, cache_hits, cache_misses, total_input_tokens, estimated_cost_usd). That logging is how I caught a subtle bug where a newline character I'd inadvertently added to the end of the system prompt variable was busting the cache on every single call β€” I watched cache_creation_input_tokens climb steadily in the logs and traced it back within 20 minutes.

If you want a quick sanity check without building a logging pipeline, just drop a Set node after your HTTP Request node in n8n and map {{ $json.usage.cache_read_input_tokens }} to a variable. Run the workflow twice within 5 minutes and check whether the second run shows a non-zero value. If it does, you're caching. If it stays at 0 both times, something is wrong β€” most likely your prompt is under 1,024 tokens or the content changed between runs.

Advanced Pattern: Cache Hierarchies with Multiple Breakpoints

Once you're comfortable with a single cache breakpoint, you can structure prompts with multiple breakpoints to maximize cache reuse across different workloads. The Anthropic API supports up to 4 simultaneous cache breakpoints per request. The strategy is to break your prompt into tiers by stability: global rules β†’ project-specific context β†’ request-specific data.

In practice, this looks like three content blocks in my system array:

python
system = [
    # Block 1: Global rules β€” never changes, cached indefinitely within TTL
    {
        "type": "text",
        "text": GLOBAL_REVIEW_RULES,         # ~2,000 tokens
        "cache_control": {"type": "ephemeral"}
    },
    # Block 2: Project context β€” changes per-project, separate cache entry
    {
        "type": "text",
        "text": project_specific_context,    # ~1,500 tokens, varies by project
        "cache_control": {"type": "ephemeral"}
    },
    # Block 3: Dynamic β€” current schema, feature flags, no cache
    {
        "type": "text",
        "text": dynamic_runtime_context      # ~500 tokens, changes per-run
        # No cache_control here
    }
]

With this layout, Block 1 stays cached across all projects. If I switch to a different project (changing Block 2), Block 1's cache remains valid and only Block 2 incurs a write cost. The dynamic block (Block 3) is always re-processed. For my workflows that handle 3–4 different project codebases, this tiered approach cuts write costs by another 30–40% compared to a single monolithic cached block, because the 2,000-token global block is almost always a cache hit regardless of which project I'm working on.

I also use this pattern for the custom MCP server I built for Claude Code β€” described in my post on building a custom MCP server β€” where the server injects project context into Claude's system prompt on every tool call. Tiered caching keeps that integration fast even when project context changes frequently.

Pitfalls That Cost Me Real Money

These are the specific mistakes I made β€” or that developers on my team made β€” before we tightened up our caching implementation.

Prompt below 1,024 tokens: The API silently ignores cache_control on blocks under the minimum. You get no error, no warning, and cache_read_input_tokens stays zero. Always verify token count before assuming you're caching. I keep a quick utility function in our shared library that logs a warning when a cached block is close to the threshold.

Whitespace changes bust the cache: The cache key is the exact byte sequence of the cached content. A trailing newline, an extra space, or a template string that renders differently on different runs will bust the cache every time. I had an n8n workflow where the system prompt was being pulled from a Postgres column, and the column had been created with a default trailing newline. Took me two days to find that one. Now I always .trim() any content block before sending it to the API.

The 5-minute TTL means cold-start penalties for low-traffic automations: If your workflow runs once per hour, you'll pay the cache write cost every run, since the cache expires between calls. In that case, caching only makes sense if your cached block is genuinely large and the 25% write surcharge is still less than the full-price read would have been. For most large prompts it still pencils out, but check the math for your specific token counts and call frequency.

Caching doesn't touch output tokens: Output tokens are always billed at full price, cache or no cache. If you're generating long responses, your cost savings from caching only apply to the input side. Keep this in mind when estimating ROI β€” if your workflows are output-heavy relative to input, caching's impact is smaller.

Model version is part of the cache key: Switching from claude-sonnet-4-5 to claude-opus-4-5 invalidates all your cached entries. This caught us during a workflow where I temporarily upgraded the model for testing and then switched back β€” the cache was cold for both transitions. Build model version changes into your cost projections whenever you're doing A/B tests.

If you're on the hardware side and looking to bulk up RAM so you can run heavier workloads locally alongside your API integrations, I've been recommending the Crucial 32GB DDR4 SODIMM (~$59) for mini PC upgrades. When I expanded my Beelink to 32GB last year it immediately opened up running a 7B Ollama model for local tasks while keeping all my other services live β€” which lets me route low-stakes classification tasks locally and reserve the Claude API (with caching) for the quality-critical work.

Putting It All Together

Prompt caching is one of those API features that sounds like a minor optimization but compounds significantly in production. When I look at my Claude API usage dashboard over the last 6 months, roughly 65% of my total input tokens are now cache reads β€” costing me a tenth of what they would have otherwise. That's not a rounding error; it's the difference between scaling up my automation portfolio being financially sustainable or not.

The implementation is genuinely low-friction. Add cache_control to your stable content blocks, log the usage fields to verify it's working, and structure your prompts with stability tiers if you're handling multiple contexts. Everything else β€” the cache TTL, the invalidation logic, the storage β€” Anthropic handles on their end.

If you want to see the full n8n workflow JSON for my code review automation with caching built in, drop a comment or reach out directly. I'm also happy to walk through more advanced patterns like caching large document corpora for RAG-adjacent workflows β€” something I've been experimenting with over the past few weeks that's opened up some interesting possibilities for self-hosted AI pipelines.

Need AI tools integrated into your dev workflow?

I build custom AI automation pipelines with Claude API, n8n, and local LLMs for development teams. Let's talk β†’

Ricardo Gil is a full-stack developer (.NET/Angular) with 6+ years of experience. He uses AI tools daily β€” Claude Code, n8n automations, and local LLMs via Ollama on his homelab. More about Ricardo β†’
πŸ“¬Weekly Newsletter

Get the best home lab & AI content

No spam. One email per week. Unsubscribe anytime.

Share this article