Context Caching in ADK: Stop Paying for the Same Tokens Every Turn
How caching a large, stable prompt prefix cuts latency and cost — and the ADK config that decides when it pays off.
Every turn, a chat agent quietly re-sends the same material: a long system persona, a fixed reference document, a big tool schema. The model re-reads and re-bills all of it, even though not a single byte changed since the last turn. Context caching stores that stable prefix on the model side so subsequent turns re-process and re-charge only the new tokens. On long, repetitive prompts it is a straight cut to both cost and latency, and in Google’s Agent Development Kit (ADK) it is one line of config.
The mental model: a prompt has a head and a tail
Think of every request as two regions. The head is stable — the same system instruction and reference context on turn 1 and turn 50. The tail changes every turn — the templated instruction, history, the new user message. Caching is a lever on the head: reuse the identical prefix instead of re-sending it.
flowchart LR
subgraph Head["stable prefix — cacheable"]
S["system instruction<br/>fixed doc / tool schema"]
end
subgraph Tail["changes every turn"]
I["templated instruction"]
H["conversation history"]
U["new user turn"]
end
S --> I --> H --> U
When it pays off (and when it doesn’t)
| Situation | Cache? | Why |
|---|---|---|
| Long, stable system prompt reused every turn | Yes | The prefix is identical across turns |
| A fixed document or large tool schema in context | Yes | Re-processing it each turn is wasted work |
| First turn of a session | No | There is no prior token count to cache against yet |
| Short, single-turn prompts | No | The prefix never reaches the model’s minimum |
| Prefix that changes every turn | No | Nothing stable to reuse |
The pattern: caching wins when a large, stable prefix dominates a multi-turn prompt. Below the model’s token floor the storage overhead outweighs the saving — which is exactly why ADK refuses to cache short prompts.
The config
In Python ADK, caching is an App-level feature. You attach a ContextCacheConfig to the App, and every LlmAgent under it inherits the policy — you don’t configure it per agent.
from google.adk.apps import App
from google.adk.agents import LlmAgent
from google.adk.agents.context_cache_config import ContextCacheConfig
cfg = ContextCacheConfig(
cache_intervals=5, # how many invocations reuse one cache before refresh
ttl_seconds=600, # how long the cache lives
min_tokens=2048, # floor on the previous request's prompt tokens
)
agent = LlmAgent(
name="cached_assistant",
model="gemini-flash-latest",
instruction="You are a helpful assistant with a long, stable system prompt.",
)
app = App(name="caching_demo", root_agent=agent, context_cache_config=cfg)
The defaults: cache_intervals is 10, ttl_seconds is 1800 (30 minutes), min_tokens is 0. Leaving context_cache_config=None — the default — disables caching entirely.
The caching decision, per turn
Caching doesn’t fire indiscriminately. ADK applies a rule with three facts baked in, and it’s worth understanding because it explains every “why didn’t my prompt cache?” surprise:
- No cache on turn 1. Caching needs the previous request’s token count, which doesn’t exist yet on the first turn. Caching starts on the second turn at the earliest.
- A floor applies. The prefix must clear both your
min_tokensand Gemini’s own model-specific minimum — 2048 tokens for Gemini 2.5, 4096 for Gemini 3 — whichever is larger. - Caches expire. A cache is reused up to
cache_intervalsinvocations and up tottl_secondsof wall-clock time before it is refreshed.
That rule is pure and testable. Expressed directly:
def should_cache(cfg, model, turn, prev_prompt_tokens):
if turn <= 1:
return False, "first turn — no prior token count yet"
floor = max(cfg.min_tokens, model_min_tokens(model)) # 2048 or 4096
if prev_prompt_tokens < floor:
return False, f"prefix {prev_prompt_tokens} tok below minimum {floor}"
return True, "stable prefix reused from cache"
Making the prefix genuinely stable: static_instruction
Caching only reuses what is byte-for-byte identical across turns, so it pays to keep the top of your prompt truly fixed. LlmAgent.static_instruction holds non-templated content — a persona, a policy, a reference document — that ADK sends verbatim as the system instruction at the very start of every request, with no {placeholder} substitution. That is precisely the region the cache can reuse. Setting it also relocates the ordinary, templated instruction to user content after the static prefix, so the cacheable region ends cleanly before anything varies per turn.
agent = LlmAgent(
name="prefixed_assistant",
model="gemini-flash-latest",
# Fixed and literal -> the stable cache PREFIX (system instruction, sent first).
static_instruction="You are ACME Corp's support agent. Never disclose internal pricing.",
# Templated, re-resolved each turn -> the CHANGING tail, placed after the prefix.
instruction="Reply in a {tone} tone.",
)
One caveat: the Live API ignores static_instruction — it has its own cache mechanism.
The Go picture (an honest note)
ADK is a dual-language framework, but this feature is asymmetric. adk/v2 v2.0.0 has no first-class ContextCacheConfig and no StaticInstruction field — context caching lives on the Python side. On the Go side, adk-go only observes caching: it records UsageMetadata.CachedContentTokenCount in telemetry. To manage caches yourself in Go, the underlying google.golang.org/genai SDK exposes manual CachedContent create/get/delete.
The decision rule, though, is language-independent — it’s just data plus a pure function. The same logic in Go:
func ShouldCache(cfg CacheConfig, model string, turn, prevPromptTokens int) (bool, string) {
if turn <= 1 {
return false, "first turn — no prior token count yet"
}
floor := cfg.MinTokens
if m := ModelMinTokens(model); m > floor { // 2048 for gemini-2.5, 4096 for gemini-3
floor = m
}
if prevPromptTokens < floor {
return false, fmt.Sprintf("prefix %d tok below minimum %d", prevPromptTokens, floor)
}
return true, "stable prefix reused from cache"
}
A neighbour: context compaction
Caching reuses the stable head of the prompt. Compaction attacks the growing tail: as a session accumulates events, ADK can summarize old ones so the history keeps fitting the context window. It is a separate, App-level policy (experimental in current ADK), attached via App(events_compaction_config=EventsCompactionConfig(...)) — a runtime rewrite of session events, not model-side token reuse. The two compose cleanly: caching cuts the cost of the stable prefix, compaction bounds the growing history.
You can read the field reference for both under App in the official ADK docs at google.github.io/adk-docs. The takeaway: reach for caching when a large, stable prefix dominates a multi-turn conversation — and let ADK’s floor decide when it actually pays off.
Next in the series: describing agents declaratively with Agent Config (YAML).