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
'prompts' => [
    // Group 1: Static content — one breakpoint at the end
    [
        'cache' => ['type' => 'ephemeral', 'ttl' => '1h'],
        'prompts' => [
            IrisStaticPrompt::class,
            AutonomousExecutionPrompt::class,
            SkillsPrompt::class,
        ],
    ],

    // Group 2: Pinned content — one breakpoint at the end
    [
        'cache' => ['type' => 'ephemeral'],
        'prompts' => [
            PinnedSkillsPrompt::class,
            PinnedPromptsPrompt::class,
        ],
    ],

    // Standalone prompts — no caching (dynamic per-request content)
    MemoryPrompt::class,
    SummaryPrompt::class,
    CalendarPrompt::class,
    CurrentTimePrompt::class,
],

There are two types of entries in the prompts array:

Cache Groups (Arrays)

An array entry 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.

Standalone Entries (Strings)

A plain class name string is a standalone prompt with no caching. Use these for dynamic, per-request content like memories, summaries, and calendar events that change between requests.

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

Here's how the default configuration maps to actual cache breakpoints in the API request:

┌─ Group 1 (cached, 1h TTL) ──────────────────┐
│  IrisStaticPrompt                            │
│  AutonomousExecutionPrompt                   │
│  SkillsPrompt                      ← BP #1  │
└──────────────────────────────────────────────┘
┌─ Group 2 (cached, ephemeral) ────────────────┐
│  PinnedSkillsPrompt                          │
│  PinnedPromptsPrompt               ← BP #2  │
└──────────────────────────────────────────────┘
  MemoryPrompt                       (dynamic)
  SummaryPrompt                      (dynamic)
  CalendarPrompt                     (dynamic)
  CurrentTimePrompt                  (dynamic)

Breakpoint #1 caches everything from the start of the system prompt through the end of SkillsPrompt. This covers the core identity, autonomous execution behavior, and skill definitions — content that rarely changes between requests.

Breakpoint #2 caches everything from the start through PinnedPromptsPrompt. Because caching is prefix-based, this breakpoint extends the cached region to include the pinned skills and prompts. If no skills are pinned (both prompts return empty), this group doesn't consume a breakpoint at all.

The remaining prompts — memories, summaries, calendar, and current time — are dynamic. They change with every request, so caching them would just waste write costs on content that never gets a cache hit.

That leaves 2 breakpoints unused out of the 4 available, giving you room to add your own cached groups for custom content.

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 an hour (pinned skills, semi-dynamic content)
'1h'2x normal input costTruly static content that rarely changes (core identity, behavior rules)

When you omit ttl, it defaults to '5m'. The default Iris config uses '1h' for Group 1 (static identity and skills) because that content almost never changes during a session, and the longer TTL means fewer cache misses over an hour of conversation. Group 2 uses the default '5m' because pinned skills can change when a user pins or unpins a skill.

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. For truly static content, '1h' almost always pays for itself.

Customizing Cache Groups

All prompt customization goes in config/iris-custom.php. The prompts key replaces the default list entirely, so you'll need to include the core prompts you want to keep.

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'],
            'prompts' => [
                App\Prompts\PinnedSkillsPrompt::class,
                App\Prompts\PinnedPromptsPrompt::class,
            ],
        ],
        App\Prompts\MemoryPrompt::class,
        App\Prompts\SummaryPrompt::class,
        App\Prompts\CalendarPrompt::class,
        App\Prompts\CurrentTimePrompt::class,
    ],
];

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

If you have custom content that changes at a different cadence than the core prompts, give it its own group:

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'],
            'prompts' => [
                App\Prompts\PinnedSkillsPrompt::class,
                App\Prompts\PinnedPromptsPrompt::class,
            ],
        ],
        // New group: project context that updates daily
        [
            'cache' => ['type' => 'ephemeral'],
            'prompts' => [
                App\Extensions\Prompts\ActiveProjectsPrompt::class,
                App\Extensions\Prompts\SprintGoalsPrompt::class,
            ],
        ],
        App\Prompts\MemoryPrompt::class,
        App\Prompts\SummaryPrompt::class,
        App\Prompts\CalendarPrompt::class,
        App\Prompts\CurrentTimePrompt::class,
    ],
];

This uses 3 of your 4 available breakpoints. The project context changes less frequently than memories or calendar events, so caching it saves tokens across a conversation session.

Removing Caching Entirely

If you're not using Anthropic (or just don't want to think about caching), list all prompts as standalone strings:

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\MemoryPrompt::class,
        App\Prompts\SummaryPrompt::class,
        App\Prompts\CalendarPrompt::class,
        App\Prompts\CurrentTimePrompt::class,
    ],
];

No groups, no breakpoints, no caching. Everything works the same — you just don't get the cost savings from Anthropic's prompt cache.

Moving Prompts Between Groups

You can reorganize which prompts share a breakpoint. For example, if you want pinned skills cached alongside the static content:

php
// config/iris-custom.php
return [
    'prompts' => [
        // Single group for all cacheable content
        [
            '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\MemoryPrompt::class,
        App\Prompts\SummaryPrompt::class,
        App\Prompts\CalendarPrompt::class,
        App\Prompts\CurrentTimePrompt::class,
    ],
];

This uses just 1 breakpoint and caches the entire static prefix. The tradeoff: if pinned skills change, the entire group's cache is invalidated. Whether that matters depends on how often you change pinned skills mid-conversation.

The 4-Breakpoint Limit

Anthropic enforces a hard limit of 4 cache_control blocks per API request. Iris maps each cache group (an array entry with a cache key) to one breakpoint.

How to count your breakpoints: Count the number of array entries in your prompts config that have a cache key. That's your breakpoint count.

php
'prompts' => [
    ['cache' => [...], 'prompts' => [...]],  // Breakpoint 1
    ['cache' => [...], 'prompts' => [...]],  // Breakpoint 2
    SomePrompt::class,                        // No breakpoint
    ['cache' => [...], 'prompts' => [...]],  // Breakpoint 3
    AnotherPrompt::class,                     // No breakpoint
],
// Total: 3 breakpoints ✓

What happens if you exceed 4: Iris doesn't throw an error. The SystemPromptBuilder applies cache breakpoints to the first 4 groups that have a cache key and silently skips caching for any additional groups. Those extra groups still render their prompts normally — they just don't get cached.

Empty groups don't count: If all prompts in a cached group return empty content, no cache_control block is produced and no breakpoint is consumed. This is why the default config can safely include the pinned skills group even when no skills are pinned — when both PinnedSkillsPrompt and PinnedPromptsPrompt return empty, that group uses zero breakpoints.

WARNING

If you see the error "A maximum of 4 blocks with cache_control may be provided" from the Anthropic API, it means your config has more than 4 groups with cache keys that are all producing non-empty content. Remove caching from less important groups or consolidate prompts into fewer groups.

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.

Don't cache dynamic content. Memories, summaries, calendar events, and current time change with every request. Caching them costs extra on the write side and never gets a hit on the read side. Keep these as standalone string entries.

Fewer breakpoints is simpler. You don't need to use all 4 breakpoints. One breakpoint on the last static prompt already caches everything before it. Add more breakpoints only when you have content at different stability cadences (e.g., core identity changes yearly, pinned skills change weekly).

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 2 breakpoints and covers the common case well. Only add more groups if you have substantial custom static content that you want cached at a different TTL.

Troubleshooting

"A maximum of 4 blocks with cache_control" Error

This means the Anthropic API received more than 4 cache_control blocks. Count the groups in your prompts config that have a cache key. If more than 4 of them are producing non-empty content, you've exceeded the limit.

Fix: Consolidate cached groups or remove caching from less critical groups. Remember that empty groups don't count, so a group might be under the limit in some cases (no pinned skills) but exceed it in others (skills pinned and all groups producing content).

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 cached groups and into a standalone entry. Only truly stable content 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.