If you send a big fixed prefix on every call — a long system prompt, a tool schema, a retrieved document — OpenAI has cached it for you automatically since 2024, no code required. GPT-5.6, generally available July 9, keeps that but bolts a steering wheel onto it. You can now draw the cache boundaries yourself with a prompt_cache_options object and per-block prompt_cache_breakpoint markers. The upside is control. The catch is that one of the most natural places to put a breakpoint — a tool's output — is accepted by the API and then silently never cached.
Here is the whole surface, and the one place it will bite you.
The four things you set#
Explicit caching on GPT-5.6 is four fields, not a rewrite. This is the shape of a request that caches a stable prefix and leaves the volatile tail uncached:
resp = client.responses.create(
model="gpt-5.6-terra",
prompt_cache_key="agent-v3:tenant-42", # required for reliable matching
prompt_cache_options={
"mode": "explicit", # only YOUR breakpoints cache
"ttl": "30m", # the only supported value (also default)
},
input=[
{
"role": "system",
"content": [
{"type": "input_text", "text": SYSTEM_PROMPT + TOOL_DOCS,
"prompt_cache_breakpoint": True}, # <- cache everything up to here
],
},
{"role": "user", "content": USER_TURN}, # volatile: stays uncached
],
)
Read mode: "explicit" carefully, because it is the whole point. It disables OpenAI-managed breakpoints — the automatic ones — so that only the blocks you tag with prompt_cache_breakpoint participate in caching. Leave mode off and you get the old implicit behavior; set it to explicit and you have told the service to stop guessing and cache exactly where you drew the line, and nowhere else.
ttl is almost a formality today: the only supported value is 30m, and it is also the default. It sets the minimum lifetime of the breakpoints a request writes — a floor, not a countdown. A prefix that keeps getting hit stays warm well past 30 minutes; the value just guarantees it won't be evicted sooner. It is worth setting explicitly anyway, so the intent is legible in the code.
prompt_cache_key is the one that quietly became mandatory. On GPT-5.6 and later you set it to get reliable matching, and it governs both implicit and explicit caching — the service matches the key against the exact prompt prefix at each breakpoint. Skip it and matching gets flaky under concurrency.
The math didn't change — and it still says yes#
The economics are the same as before, which is the good news, because they already justified caching:
A cache write costs 1.25x the uncached input rate; a cache read costs 90% less. So a reused prefix pays back its write surcharge on the very second hit and is pure savings after that.
That break-even is the entire decision. An explicit breakpoint is only worth placing on content you will reuse before it expires. On a stable prefix hit dozens of times an hour — a system prompt, a fixed tool catalog, a cached RAG chunk — it is close to free money. On a prefix you touch once, mode: explicit just made you pay 1.25x for a cache nobody reads. The value of the new surface is that you decide which is which, instead of letting managed breakpoints spend the budget for you. (For the deeper cost framing across providers, see implicit vs explicit prompt caching and the cross-provider pricing breakdown.)
The gotcha: breakpoints on tool outputs never write#
Here is the non-obvious part, and it lands hardest on exactly the people reading this — anyone building a tool-calling agent.
A multi-turn agent accumulates function_call_output blocks: the results of every tool it ran this session. Those can be large, and reusing them across turns is precisely what you'd want to cache. So the natural move is to drop a prompt_cache_breakpoint onto a function_call_output. The API schema allows it. The request returns no 400. Everything looks fine.
It never writes a cache. Per a reported limitation on the Responses API as of the July GA, a breakpoint on the text content inside function_call_output.output is accepted and silently produces zero cache writes. There is no error to catch — you just quietly pay full input price for that block on every turn while believing it's cached.
The fix is to move the breakpoint off the tool output and onto the stable prefix that precedes it — the system prompt and tool definitions, which are the biggest, most-reused block anyway. Structure the conversation so the durable, cacheable content sits ahead of a single breakpoint and the tool results trail after it, uncached. That's less convenient than caching each tool result in place, but it's the only layout that actually writes.
Verify it, don't trust it#
Because the failure mode is silent — no error, wrong billing — the only safe workflow is to check the write happened. After a request that should have populated a cache, read the cached-token count back off the usage object. If two identical requests in a row both report zero cached input tokens, a breakpoint isn't catching: you either put it on a function_call_output, fell below the model's minimum cacheable length, forgot the prompt_cache_key, or let a volatile byte (a timestamp, an unsorted JSON dump, a reordered tool list) drift into the prefix and shift it.
One more operational limit worth pinning to the wall: keep each prompt_cache_key under roughly 15 requests per minute. Above that, some requests start missing the cache regardless of how clean your prefix is. For a high-volume endpoint, partition traffic across multiple keys with a stable mapping — the same logical prefix always routes to the same key — so requests that share a prefix keep landing on the same cache. This is the same discipline the long-running task and batching surfaces reward: make the boundaries explicit, then measure that they held.
The headline is small and the money is not. GPT-5.6 handed you the cache controls the older models kept to themselves — mode, ttl, prompt_cache_key, and a breakpoint you place by hand. Use them on the prefixes you actually reuse, keep them off your tool outputs until the write bug is fixed, and read the usage numbers back to prove the discount is real.



