Skip to content

Cache Breakpoints ​

Cache breakpoints let you tell Anthropic's API to cache stable parts of your system prompt, so repeated requests reuse the cached prefix instead of re-processing it. This cuts input token costs by up to 90% and reduces latency on every message.

How Prompt Caching Works ​

Anthropic uses prefix caching: a cache breakpoint marks a point in the system prompt and tells the API "cache everything from the very start up to here." On subsequent requests, if the content before the breakpoint is byte-identical, the API serves it from cache at 10% of the normal input token cost.

The key constraint is that Anthropic allows a maximum of 4 cache breakpoints per request. If you try to set more than 4, the API returns a 400 error. This is why Iris centralizes cache management in the config rather than letting individual prompt classes set their own caching — it's too easy to exceed the limit when prompts independently decide to cache themselves.

A few things to keep in mind about how caching works:

  • Caching is prefix-based. A breakpoint at position N caches everything from position 0 through N. If anything before the breakpoint changes, the cache misses entirely.
  • Cache hits are 10% of the normal input token cost. For a 50K token system prompt, that's the difference between paying for 50K tokens and paying for 5K tokens on every message.
  • Only content before or at a breakpoint is cached. Content after the last breakpoint is always processed fresh.

Cache Groups ​

Iris organizes system prompts into cache groups in the prompts config. Each group bundles related prompts together and optionally marks them for caching with a single breakpoint.

Here's the default configuration from config/iris.php:

php
// config/iris.php

// Cached system prompt — stable content, one breakpoint per group
'prompts' => [
    // Group 1: Static identity, behavior, skills (1h TTL)
    [
        'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
        'prompts' => [
            IrisStaticPrompt::class,
            AutonomousExecutionPrompt::class,
            SkillsPrompt::class,
        ],
    ],

    // Group 2: Pinned content and summaries (1h TTL)
    [
        'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
        'prompts' => [
            PinnedSkillsPrompt::class,
            PinnedPromptsPrompt::class,
            SummaryPrompt::class,
        ],
    ],
],

// Context tail — dynamic per-request content, never cached
'context_prompts' => [
    CrossThreadContextPrompt::class,
    MemoryPrompt::class,
    CalendarPrompt::class,
    WeatherPrompt::class,
    CurrentTimePrompt::class,
],

// History cache breakpoint
'history_cache' => [
    'enabled' => true,
    'ttl' => '1h',
    'min_tokens' => 20000,
],

Cache Groups (Arrays) ​

An array entry in prompts with cache and prompts keys defines a cache group. The group produces one cache breakpoint, applied to the last non-empty message in the group.

KeyTypeRequiredDescription
cache.typestringYesCache type. Use 'ephemeral' (the only currently supported type).
cache.ttlstringNoCache duration. Either '5m' (default) or '1h'. See Cache TTL Options.
promptsarrayYesOrdered list of prompt class names to render in this group.

IMPORTANT

Dynamic, per-request content doesn't belong in prompts at all — it goes in context_prompts, the context tail. Putting volatile content in the cached system prompt would invalidate the cache on every request.

Where the Breakpoint Lands ​

Within a cache group, the breakpoint is applied to the last non-empty message produced by the group's prompts. If a prompt's content() returns an empty string, it doesn't generate a message and doesn't affect breakpoint placement. This means:

  • If a group has 3 prompts and all produce content, the breakpoint lands on the 3rd prompt's message.
  • If the last prompt in a group returns empty content, the breakpoint moves to the previous non-empty prompt's message.
  • If all prompts in a group return empty content, no breakpoint is consumed. The group effectively becomes invisible.

How Groups Map to Breakpoints ​

Iris uses three of Anthropic's four breakpoints by default, spread across the system prompt and the conversation history:

SYSTEM PROMPT
┌─ Group 1 (cached, 1h TTL) ──────────────────┐
│  IrisStaticPrompt                            │
│  AutonomousExecutionPrompt                   │
│  SkillsPrompt                      ← BP #1   │
└──────────────────────────────────────────────┘
┌─ Group 2 (cached, 1h TTL) ──────────────────┐
│  PinnedSkillsPrompt                          │
│  PinnedPromptsPrompt                         │
│  SummaryPrompt                     ← BP #2   │
└──────────────────────────────────────────────┘

CONVERSATION HISTORY
   … stable turns …                  ← BP #3 (1h)
   last 2 turns (tool output in flux, not cached)

CONTEXT TAIL (prepended to newest message — never cached)
   CrossThread · Memory · Calendar · Weather · CurrentTime

Breakpoint #1 caches everything from the start of the system prompt through SkillsPrompt — core identity, behavior, and skill definitions that rarely change.

Breakpoint #2 extends the cached region through SummaryPrompt, picking up pinned content and conversation summaries. If nothing's pinned and there are no summaries, this group produces no messages and consumes no breakpoint.

Breakpoint #3 caches the stable portion of the conversation history — everything except the most recent context.prune_protect_turns turns, whose tool output is still being pruned down and isn't byte-stable yet. This is what makes history caching work; see The History Cache Breakpoint. It's gated by history_cache.min_tokens so short threads skip the write cost.

The context tail sits after everything cached, so its per-request churn never invalidates the prefix. That leaves one of the four breakpoints free for a custom cached group.

NOTE

Everything cached uses the '1h' TTL. Iris deliberately does not add a shorter-lived breakpoint on the tool-result tail: Anthropic requires non-increasing TTLs along the prefix, and a 5-minute tool-result block can land ahead of the 1-hour history breakpoint, which the API rejects. In-loop tool-step caching is left off for that reason.

The Context Tail ​

Per-request content — recalled memory, calendar, weather, the current time, and other threads' activity — changes on almost every turn. If it lived in the cached system prefix, it would invalidate the cache for everything behind it on every message. So it doesn't live there.

Instead, the prompt classes in context_prompts render into a single fenced <context> block prepended to your newest user message at request-build time:

text
<context>
The following is ambient context for this turn ... not part of the user's message.

...recalled memory, calendar, weather, time, cross-thread briefs...
</context>

...your actual message...

Two things make this work:

  • It's never persisted. The block is added when the request is built, only to the newest message. The stored conversation row keeps your original text, so replayed history stays byte-identical and cacheable.
  • It sits last. Everything volatile is behind the history breakpoint, so the cached prefix never changes because of it.

context_prompts is a flat list of prompt class names — no cache groups, because nothing here is ever cached across turns.

The History Cache Breakpoint ​

Caching the system prompt only gets you so far; in a long conversation the history dwarfs it. Breakpoint #3 caches the history too — but that only works if the history is byte-stable from the front, and two mechanisms keep it that way:

  • A summary anchor. History loads from the latest summary's boundary forward, not from a sliding window. Summarized turns are represented by the Summary Prompt, not resent raw, so the oldest included turn doesn't shift every message. See Context Management.
  • The protected-turns gap. The breakpoint lands behind the most recent context.prune_protect_turns turns, whose tool output is still being pruned down as they age and isn't final yet. Caching only the settled turns avoids writing an entry that's about to change.

The history_cache config controls it:

KeyDefaultEffect
history_cache.enabledtrueMaster switch for breakpoints #3 and #4
history_cache.ttl'1h'TTL for the history breakpoint; keep it at or below the system groups' TTL
history_cache.min_tokens20000Skip the breakpoint until history is at least this large

NOTE

The min_tokens gate matters because a cache write costs more than a plain read. On a short thread that won't be revisited, writing a history cache entry you'll never read back is wasted money — so Iris waits until the history is big enough to pay for itself.

Cache TTL Options ​

The ttl option controls how long cached content lives and what it costs to write:

TTLWrite CostBest For
'5m' (default)1.25x normal input costContent that might change within the hour
'1h'2x normal input costContent stable across a session (identity, pinned skills, summaries, history)

When you omit ttl, it defaults to '5m'. The default Iris config uses '1h' for both system groups and the history breakpoint.

IMPORTANT

TTLs must be non-increasing along the prefix — a breakpoint can't have a longer TTL than one before it. Because the history breakpoint (1h) sits behind both system groups, both groups must also be '1h'. Set an earlier group to '5m' while a later breakpoint is '1h' and Anthropic rejects the request.

TIP

The 1h TTL costs 2x to write but only 0.1x to read. If content stays stable for an hour, you break even after just one cache hit and save on every subsequent request.

Customizing Cache Groups ​

All prompt customization goes in config/iris-custom.php. The prompts key replaces the default system-prompt list entirely, so include the core prompts you want to keep. The examples below customize prompts only — context_prompts keeps its default unless you set it too (setting it replaces that list as well).

Adding a Custom Prompt to an Existing Group ​

If your custom prompt is static content (team info, project context, style guides), add it to an existing cached group so it shares that group's breakpoint:

php
// config/iris-custom.php
return [
    'prompts' => [
        [
            'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
            'prompts' => [
                App\Prompts\IrisStaticPrompt::class,
                App\Prompts\AutonomousExecutionPrompt::class,
                App\Prompts\SkillsPrompt::class,
                App\Extensions\Prompts\TeamStyleGuidePrompt::class, // Added to Group 1
            ],
        ],
        [
            'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
            'prompts' => [
                App\Prompts\PinnedSkillsPrompt::class,
                App\Prompts\PinnedPromptsPrompt::class,
                App\Prompts\SummaryPrompt::class,
            ],
        ],
    ],
    // context_prompts keeps its default (memory, calendar, weather, time, briefs)
];

This doesn't use an additional breakpoint — the TeamStyleGuidePrompt content is included in the cached region alongside the other static prompts.

Creating a New Cached Group ​

With history caching on, the two default system groups plus the history breakpoint use three of the four slots, so there's room for one more cached group. Add it directly:

php
// config/iris-custom.php
return [
    'prompts' => [
        [
            'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
            'prompts' => [
                App\Prompts\IrisStaticPrompt::class,
                App\Prompts\AutonomousExecutionPrompt::class,
                App\Prompts\SkillsPrompt::class,
            ],
        ],
        [
            'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
            'prompts' => [
                App\Prompts\PinnedSkillsPrompt::class,
                App\Prompts\PinnedPromptsPrompt::class,
                App\Prompts\SummaryPrompt::class,
            ],
        ],
        // Your group: project context that updates daily
        [
            'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
            'prompts' => [
                App\Extensions\Prompts\ActiveProjectsPrompt::class,
                App\Extensions\Prompts\SprintGoalsPrompt::class,
            ],
        ],
    ],
];

That's 3 system groups + history = 4. If you need a second custom cached group, free a slot by merging the two default groups into one (see Moving Prompts Between Groups); otherwise Iris throws a clear config error before the request is sent (see The 4-Breakpoint Limit).

Removing Caching Entirely ​

If you're not using Anthropic (or just don't want to think about caching), drop the cache keys so no breakpoints are set, and turn off history caching:

php
// config/iris-custom.php
return [
    'prompts' => [
        App\Prompts\IrisStaticPrompt::class,
        App\Prompts\AutonomousExecutionPrompt::class,
        App\Prompts\SkillsPrompt::class,
        App\Prompts\PinnedSkillsPrompt::class,
        App\Prompts\PinnedPromptsPrompt::class,
        App\Prompts\SummaryPrompt::class,
    ],
    'history_cache' => ['enabled' => false],
];

No groups, no breakpoints, no caching. Everything works the same — you just don't get the cost savings from Anthropic's prompt cache. The context tail is never cached either way.

Moving Prompts Between Groups ​

You can reorganize which prompts share a breakpoint. Merging the two default groups caches the whole static prefix with a single breakpoint, which frees a slot if you need more than one custom cached group while history caching is on:

php
// config/iris-custom.php
return [
    'prompts' => [
        [
            'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
            'prompts' => [
                App\Prompts\IrisStaticPrompt::class,
                App\Prompts\AutonomousExecutionPrompt::class,
                App\Prompts\SkillsPrompt::class,
                App\Prompts\PinnedSkillsPrompt::class,
                App\Prompts\PinnedPromptsPrompt::class,
                App\Prompts\SummaryPrompt::class,
            ],
        ],
    ],
];

This uses just 1 breakpoint and caches the entire static prefix. The tradeoff: when pinned skills or a summary change, the whole group's cache is invalidated rather than just the second group's. Whether that matters depends on how often those change mid-conversation.

The 4-Breakpoint Limit ​

Anthropic enforces a hard limit of 4 cache_control blocks per API request. With the default setup, Iris uses three:

BreakpointSource
#1, #2The two cached groups in iris.prompts
#3The history breakpoint (history_cache.enabled)

Counting your budget: When history_cache.enabled is true, the history breakpoint reserves 1 slot, leaving 3 for cached groups in iris.prompts. With history caching off, all 4 are available for cached groups.

What happens if you exceed it: Iris checks the budget while building each request and throws a clear RuntimeException naming the problem — it does not let the request reach Anthropic and fail there. To fix it, merge cached groups in iris.prompts, or set history_cache.enabled to false to reclaim the history slot.

Empty groups still count toward the budget. The guard counts the cached groups you configure, not the ones that happen to render non-empty on a given request. That's deliberate: a config that can emit five cache_control blocks under the right conditions is a misconfiguration even when it slips under the limit today.

WARNING

If you see "A maximum of 4 blocks with cache_control may be provided" straight from the Anthropic API despite the guard, you've likely set cache options on messages outside the prompts / history_cache system. Keep cache placement in the config.

Best Practices ​

Group static content together. Prompts that share the same stability cadence should share a group. If three prompts all change only when you redeploy, put them in one group with 'ttl' => '1h' — one breakpoint, maximum cache reuse.

Put the most stable content first. Caching is prefix-based. If content at position 2 changes, everything after it (positions 3, 4, 5...) misses cache too, even if it hasn't changed. The default config puts the core identity prompt first because it essentially never changes.

Keep volatile content in the context tail. Memories, calendar events, weather, the current time, and cross-thread briefs change with nearly every request. They belong in context_prompts, not a cached group — caching them would cost extra on the write side and never earn a read hit. Summaries are the exception: they change only at trim time, so they stay in a cached group.

Mind the budget with history caching on. History caching claims 1 of the 4 breakpoints, leaving 3 for cached prompts groups (the default uses 2). If you need a third custom group beyond that, merge the two defaults first, or set history_cache.enabled to false.

Non-Anthropic providers ignore cache config. If you're using OpenAI, Ollama, or another provider, the cache groups and breakpoints in your config are completely harmless — they're just ignored. You can keep the grouped config format and switch providers without changing anything.

TIP

When in doubt, start with the default config. It uses three of the four breakpoints — two system groups plus the history breakpoint — and covers the common case.

Troubleshooting ​

"A maximum of 4 blocks with cache_control" Error ​

Normally Iris catches this first and throws a clear RuntimeException naming the problem (see The 4-Breakpoint Limit). If the raw Anthropic error reaches you anyway, something set cache_control outside the config — for example a custom prompt building messages by hand.

Fix: Keep cache placement in iris.prompts and iris.history_cache. With history caching on, that's up to 3 cached prompts groups plus the history breakpoint.

Cache Not Hitting ​

If you're paying full input token cost on every request despite having cache groups configured, the content before a breakpoint is likely changing between requests. Common causes:

  • A prompt in a cached group includes dynamic data (timestamps, counters, randomly ordered lists)
  • Prompt content includes data that's fetched fresh on every request
  • The user changed pinned skills, invalidating the Group 2 cache

Fix: Move any prompt with per-request content out of the cached groups and into context_prompts. Only content that's stable within a thread should be in a cached group.

Using a Non-Anthropic Provider ​

Cache groups are Anthropic-specific. If you're using OpenAI, Ollama, Gemini, or any other provider, the cache keys in your config are silently ignored. Your prompts still render in order and work correctly — you just don't get the caching optimization.

This is by design. You can keep the grouped config format even when using non-Anthropic providers, which makes it easy to switch between providers without restructuring your prompt config.

Custom Messages With Cache Options ​

If you've written a custom prompt that returns messages with cacheType or cacheTtl provider options (via an asMessages() override), those values are automatically stripped by the SystemPromptBuilder. The config is the single authority for cache placement. This prevents individual prompts from accidentally exceeding the 4-breakpoint limit.