CacheCanary

Prompt caching with Strands Agents on Amazon Bedrock

Strands Agents is AWS's open-source agent SDK, and Bedrock is its default model provider. I captured the exact requests Strands 1.58.1 sends to Bedrock while it runs its real agent loop, checked them with CacheCanary, and ran every case on live Bedrock (Claude Sonnet 4.6, us-west-2, October 2026, re-checked October 30).

The short version: caching is off unless you turn it on. Once it's on, it works well, including steps with many tool calls at once, but long conversations quietly throw the cache away.

The setup I'd use

For most agents, turn caching on and swap the conversation manager for one that trims old messages in bulk (finding 3 explains why):

from strands import Agent
from strands.agent.conversation_manager import SlidingWindowConversationManager
from strands.models import BedrockModel
from strands.models.model import CacheConfig


class TrimInChunks(SlidingWindowConversationManager):
    """Trim old messages in bulk, so the cached start rarely changes."""

    def __init__(self, window_size: int = 40, trim_to: int = 20, **kwargs):
        super().__init__(window_size=window_size, **kwargs)
        self.trim_to = trim_to

    def apply_management(self, agent, **kwargs) -> None:
        if len(agent.messages) <= self.window_size:
            return
        full, self.window_size = self.window_size, self.trim_to
        try:
            self.reduce_context(agent)
        finally:
            self.window_size = full

    def restore_from_session(self, state):
        # Let sessions saved with the default manager switch to this one.
        if state.get("__name__") == "SlidingWindowConversationManager":
            state = {**state, "__name__": type(self).__name__}
        return super().restore_from_session(state)


agent = Agent(
    model=BedrockModel(
        model_id="us.anthropic.claude-sonnet-4-6",
        cache_config=CacheConfig(strategy="auto"),
    ),
    system_prompt=SYSTEM_PROMPT,
    tools=tools,
    conversation_manager=TrimInChunks(),
)

What I found

1. Caching is off by default

A plain BedrockModel(model_id=...) sends no cache points, so every call pays full price for the whole prompt. A change to turn caching on by default was proposed and closed without being merged. On live Bedrock, the same system prompt sent twice:

SetupSecond call: read from cachePaid at full price
No cache_config (the default)01,996
CacheConfig(strategy="auto")1,9933

With strategy="auto", Strands puts one cache point after the system prompt and one at the end of the newest user message. After a round of tool calls, the newest user message is the one holding the tool results.

2. Many tool calls at once: fine since October 2026

Bedrock only looks back about 20 positions from a cache point to find the previous cache entry. When I first tested this on October 7 2026, it counted every tool call and every tool result as its own position, so a step that ran 11 tools at once lost the cached conversation and wrote it again at 1.25 times the normal price (issue #3348). By October 28 Bedrock counted a group of tool calls side by side as one position, and the same for their results, as Anthropic documents for its own API. The same requests on live Bedrock, with a long customer history in the first message:

Next stepRead from cacheWritten
10 tool calls at once6,9331,783
11 tool calls at once6,929 (on October 7: 2,221, the system prompt only)1,958

Through Strands' own agent loop in auto mode, each step read almost the whole previous request from the cache:

StepRead from cacheSize of the previous request
12 tool calls at once6,9356,938
1 tool call9,0689,069
20 tool calls at once9,2479,248
A follow-up question from the user12,77712,778
3 tool calls at once12,79312,796
40 tool calls at once13,35113,352

Tokens. The last few tokens of each request are always new, so the cache can't hold them.

So auto mode is enough, and the hook I used to recommend here isn't needed any more. With a live model choosing its own tools, it called 6 at once, then 2, and the second question read 7,115 tokens from the cache and wrote 496. A step can still lose the cache if it adds more than about 20 other blocks at once, like 22 or more separate text blocks; cachecanary diff counts positions the way Bedrock does now.

3. Long conversations lose the cache on every turn

By default Strands keeps the last 40 messages. Once a conversation is longer, it drops the oldest messages at the end of every call to the agent, so the start of the conversation changes every turn and nothing after the system prompt can be read from the cache. On live Bedrock, a chat with full-length answers, from the 22nd question on:

Conversation managerRead from cacheWritten again
Default (40 messages)2,225 on every turn15,871 on every turn
TrimInChunks()10,323, then 11,100, 11,877, 12,654777 on each turn

TrimInChunks (in the setup above) lets the history grow to 40 messages and then trims it back to 20 in one go, so the start of the conversation changes about once every 10 turns instead of every turn. Over 45 questions, the default manager changed it on 24 calls and TrimInChunks on 3. It keeps 20 to 40 messages instead of always 40, so the model sees a little less history; raise both numbers if you need more. Without tools or with them, it works the same, and it works with per_turn and pin_first. The last method matters if you save sessions: Strands checks the manager's class name when it restores one, and without it, a session saved with the default manager fails to load ("Invalid conversation manager state").

4. Application inference profiles need strategy="anthropic"

Auto mode only caches when the model ID contains "claude" or "anthropic". An application inference profile ARN contains neither, so Strands logs a warning ("cache_config is enabled but this model does not support automatic caching") and sends no cache points. The maintainers chose this on purpose. If the profile points to a Claude model, say so:

BedrockModel(
    model_id="arn:aws:bedrock:REGION:ACCOUNT:application-inference-profile/ID",
    cache_config=CacheConfig(strategy="anthropic"),
)

On live Bedrock with a profile for Sonnet 4.6, the second call read 0 tokens from the cache with "auto" and 1,991 with "anthropic".

5. Two cache lifetime settings that Bedrock rejects

Bedrock requires a 1-hour cache point to come before any 5-minute one, in the order tools, system prompt, messages. CacheConfig(ttl="1h", system_prompt_ttl="5m") and CacheConfig(ttl="1h", tools_ttl="5m") both put a 5-minute point first, so Bedrock rejects every request with "a ttl='1h' cache_control block must not come after a ttl='5m' cache_control block". This one is loud, but it takes your agent down until you change it (issue #3758). Use one lifetime everywhere, or give the earlier sections the longer one.

Smaller things

What Strands gets right

In auto mode Strands caches the system prompt as well as the conversation, keeps a cache point you place in the newest user message, and steps a cache point back when it would land right after a non-PDF document, which Bedrock rejects. In my tests, every request it sent was valid for Bedrock, apart from the two lifetime settings above.

Check your own agent

Save the exact requests your BedrockModel sends. This works for streaming and non-streaming calls; I checked each saved file is identical to what reached Bedrock.

import pathlib, time


def save_requests(model, folder="strands-requests"):
    """Save every request this BedrockModel sends, for cachecanary."""
    out = pathlib.Path(folder)
    out.mkdir(exist_ok=True)

    def save(params, **kwargs):
        (out / f"{time.time_ns()}.json").write_bytes(params["body"])

    for op in ("Converse", "ConverseStream"):
        model.client.meta.events.register(
            f"before-call.bedrock-runtime.{op}", save)


model = BedrockModel(
    model_id="us.anthropic.claude-sonnet-4-6",
    cache_config=CacheConfig(strategy="auto"),
)
save_requests(model)

Run a conversation, then compare two requests in a row with cachecanary diff, or check one with cachecanary lint. These are real outputs on requests Strands built:

$ cachecanary diff chat-turn20.json chat-turn21.json --model us.anthropic.claude-sonnet-4-6
history-changed: Earlier conversation history was edited (not just appended). (messages[0].content[0])

$ cachecanary lint ttl-order.json --model us.anthropic.claude-sonnet-4-6
[error] ttl-order: A 1h checkpoint appears after a 5m checkpoint. Longer TTLs must come first.

In plain words: the first pair is the default conversation manager trimming the first messages (finding 3). The second is the lifetime setting Bedrock rejects (finding 5).

Keep a few saved requests in your repo and run cachecanary lint on them in CI, and a Strands upgrade that changes your cache points shows up in a pull request instead of on the bill. Setup is on the home page.

How I tested this

Strands Agents 1.58.1 in a clean Python environment. To capture requests, I pointed Strands at a small local server standing in for Bedrock that speaks its streaming format, so Strands ran its real agent loop (tool calls, streaming, conversation trimming) and every request body was saved. The settings cases ran fully live on Bedrock (Claude Sonnet 4.6, us-west-2, October 7 2026), and I re-ran every case on October 30 2026 after Bedrock changed how it counts tool calls. A live model won't call exactly 11 tools on request, so for the agent-loop cases I sent the exact requests Strands built, in order, to live Bedrock; caching depends only on the request. Every case uses made-up prompts with a fresh random tag, so old cache entries couldn't help. The capture and the live checks are scripts in the repo, so you can run them yourself: capture and live checks. Token counts move by a few tokens from run to run because each run uses a new random tag. Strands changes quickly, so check your own version with the snippet above.

Same tests for other frameworks: LiteLLM and LangChain. More on why caching breaks on Bedrock: Five ways Claude prompt caching quietly breaks on Amazon Bedrock.

Want a quick number for your own app? Paste three totals from CloudWatch into the cache checker. It runs in your browser, nothing to install.

Using CacheCanary? I'd love to hear how it's going, good or bad, and whether you'd want the dashboard. Email me at hello@cachecanary.com. I read every message.