DEV Community

Cover image for Prompt Caching in Pydantic AI: Up to 89% Lower Input Costs
Alex Resch
Alex Resch

Posted on

Prompt Caching in Pydantic AI: Up to 89% Lower Input Costs

This article is for AI developers who want to build an AI agent with Pydantic AI that uses changing instructions and still saves costs. The problem is that Pydantic AI used to put static and dynamic instructions in the same block, so one small change broke the whole cache. Below, I show how it works today.

The problem

Static instructions stay the same for a long time, for example what the agent should do and which rules it has to follow. Dynamic instructions change depending on the situation, for example the current date.

Before my pull request (PR), every time the date changed, the agent had to pay the full price for all static and dynamic instructions again, instead of reading them from the cache for 10% of the price, even though only one dynamic instruction had changed.

I found this problem by looking for an issue the maintainers wanted, but nobody had worked on yet. Issue #4543 had no replies, and a related issue about better Anthropic caching (#3453) had been open since November 2025. So I opened PR #4865.

How prompt caching works on Anthropic

Anthropic caches the beginning of a request up to a breakpoint that you mark with cache_control. The next request can only read from the cache if everything up to this breakpoint is exactly the same.

The order is always tools first, then the system prompt, then the messages. A change at one of these levels makes the cache for this level and all levels after it invalid.

You can set at most 4 breakpoints per request and choose between a lifetime of 5 minutes or 1 hour. Writing to the cache costs 25% more than the normal input price (5 minutes) or twice as much (1 hour), but reading from it only costs 10%.

Static and dynamic instructions in Pydantic AI

The code below is a simple example of static and dynamic instructions:

from datetime import date

from pydantic_ai import Agent, RunContext

POLICY = "..."

agent = Agent(
    'anthropic:claude-haiku-5-5',
    deps_type=str,
    instructions=POLICY,  # static: a long return policy that never changes
)

@agent.instructions
def customer_context(ctx: RunContext[str]) -> str:  # dynamic: changes per request
    return f"Customer name: {ctx.deps}. Today's date: {date.today()}."
Enter fullscreen mode Exit fullscreen mode

As the comments show, POLICY is the static instruction and the function customer_context is the dynamic instruction. Pydantic AI treats everything you pass to instructions= as static and every function with @agent.instructions as dynamic. date.today() depends on the current date and the customer name comes from the request, so both change. POLICY stays the same unless you change the policy yourself.

My first approach and what the review changed

My first thought was to create a new parameter add_cache_breakpoint for @agent.instructions and a new setting anthropic_static_cache_instructions, because not everyone needs this kind of caching with Anthropic. But this setting would be in the core of the framework, so every user, including users who work with other AI providers like OpenAI or Google, would see it, although it does nothing for them.

So the maintainer, Douwe Maan, came up with the idea to make it automatic instead of adding a new setting and parameter, and reworked the PR. Now static instructions are always sorted before dynamic ones, and with the existing setting anthropic_cache_instructions=True, the cache breakpoint is placed automatically after the last static instruction. I loved the idea. You don't need a new setting, you don't have to read about it or even know about it, because you already have it.

Working together with the maintainer of Pydantic AI helped me a lot to learn about discussions and architectural design. It also taught me to keep looking for the best solution until I'm really sure I have it.

How to use it today

To cache the static instructions, you only have to add one setting to the agent from above:

from pydantic_ai.models.anthropic import AnthropicModelSettings

agent = Agent(
    'anthropic:claude-haiku-5-5',
    deps_type=str,
    instructions=POLICY,
    model_settings=AnthropicModelSettings(anthropic_cache_instructions=True),
)
Enter fullscreen mode Exit fullscreen mode

Instead of AnthropicModelSettings(anthropic_cache_instructions=True), you can also use model_settings={'cache': True}. This is the provider-independent setting for caching. On Anthropic, it also caches the tool definitions and the conversation.

To check if it works, I ran the agent three times with a different customer each time:

for customer in ['Alice', 'Bob', 'Carol']:
    result = agent.run_sync('Can I return unused headphones from category 7?', deps=customer)
    usage = result.response.usage
    print(customer, usage.input_tokens, usage.cache_write_tokens, usage.cache_read_tokens)
Enter fullscreen mode Exit fullscreen mode

These are the results with Claude Haiku 5.5 and Pydantic AI 2.60.0:

Run Input tokens Cache write Cache read
Alice 3,247 3,205 0
Bob 3,247 0 3,205
Carol 3,247 0 3,205

In the first run with Alice, the static policy is written to the cache. In the runs with Bob and Carol, 3,205 of 3,247 input tokens come from the cache, although the customer name changed. Only the remaining 42 tokens, the dynamic instructions and the question, are charged at the normal price.

For the costs, this means that the first run is about 25% more expensive, because writing to the cache costs extra. But every run after that costs about 89% less for the input. Over these three runs, that already saves about 51% of the input costs, and the more requests you have, the more you save.

Pitfalls

There are a few things you should pay attention to:

  • Don't use dynamic values in static instructions. Agent(instructions=f'... {date.today()}') looks static, but it's not, because the date changes every day. So the cache is gone every day. Put values like this in an @agent.instructions function instead.
  • Dynamic instructions that change during a conversation. If a dynamic part changes in the middle of a conversation, the static part stays in the cache, but the cached conversation after it is gone. To handle this, use @agent.instructions(name='state', on_change='append'). Then each change is appended to the message history instead of rewriting the instructions. The history gets a bit longer with every change, but the cache stays.
  • Instructions that are too short. Caching only works above a minimum length, for example 512 tokens for Claude Haiku 5.5 and up to 4,096 tokens for older models. If your instructions are shorter, nothing is cached and you don't get an error. To catch that, check cache_write_tokens and cache_read_tokens. If both are 0, nothing got cached.

Conclusion

Prompt caching only works if the beginning of your prompt stays exactly the same. So keep your static instructions static and put everything that changes in an @agent.instructions function. Pydantic AI then sorts the static instructions before the dynamic ones and places the cache breakpoint between them automatically. You only need one setting for it. In my test, every request after the first one cost about 89% less for the input.

My biggest lesson from this PR: always ask if there is an even simpler way for the developer who uses your code, without making it unnecessarily complex.

You can find all caching options in the Pydantic AI docs.

AI helped me with the English, the wording and some parts of the text. I reviewed and edited everything myself.

Top comments (0)