One of our engineers hit their monthly spend cap in two days. Our Governance Gateway did its job and stopped the spend, but the reason the cap had been reached in two days was not obvious.
The short version
- In affected workloads, Codex rewrites a large stable prefix into the prompt cache because it cannot emit the explicit breakpoint that marks the end of that prefix and creates a new cache key for every session.
- Cache writes are billed at 1.25× uncached input and cache reads at 0.10×, so a write costs 12.5× a read. On our invoice, cache writes were 90% of all GPT-5.6 spend.
- Over the same period, the same client and gateway on GPT-5.5 recorded 9.17 million input tokens and no separate cache-write charge. Codex sent the same request shape; the applicable billing changed.
- In a controlled replay, injecting the missing field at the gateway together with a cache key that is stable across sessions produced a 0% cold-request hit rate and 96–97% on warm requests, cut cache-write tokens by 65%, and cut the replayed workload’s cost by 59% at published Bedrock rates. For the same captured workload, the unmitigated path cost roughly 2.5x the mitigated one.
- Codex traffic that does not cross a gateway has no configuration-only workaround today. A general fix needs a change inside Codex, filed as openai/codex#35300.
Why cache writes became the biggest line item on GPT-5.6
Under the Bedrock rates applicable to GPT-5.5, prompt-cache writes incurred no separate charge. A client that rewrote its entire prefix into the cache on every request paid nothing for those writes, so the behavior produced no line item and no reason to investigate it. On GPT-5.6 a write is billed at 1.25× uncached input while a read is billed at 0.10×. Rewriting a prefix that could have been read now costs 12.5 times as much as reading it.
The spend cap is what made this visible in two days rather than at month end. Spend against the cap is checked and reserved before inference runs, so an unusual burn rate surfaces as a blocked request rather than as an invoice line nobody reads until the following month.
Our own gateway ledger already showed the cache-write volume, and its figures matched AWS Cost Explorer exactly. The spend we investigated is shown below, using AWS Cost Explorer data for OpenAI GPT-5.6 Sol (Amazon Bedrock Edition), complete days from 2026-07-01 to 2026-07-23, filtered to RECORD_TYPE = Usage.
| usage type | tokens | cost | share |
|---|---|---|---|
| cache write | 258.8M | $1,780.33 | 90.0% |
| output | 3.9M | $127.56 | 6.5% |
| input | 9.8M | $53.98 | 2.7% |
| cache read | 28.6M | $15.71 | 0.8% |
| total | $1,977.58 |
Write tokens outnumbered read tokens 9.1 to 1; including uncached input in the denominator, the aggregate hit rate was 9.6%. Cache reads, the outcome we actually wanted, cost $15.71 across the whole period.
The previous model generation serves as the control. Over the same period, the same client and gateway on OpenAI GPT-5.5 (Amazon Bedrock Edition) recorded 9.17M input tokens and no separate cache-write charge. Codex sent the same request shape; the difference was the billing applied to those writes.
The invoice shows the size of the problem, not the reason for it. Some share of those 258.8M write tokens is unavoidable: genuine cold starts, genuinely distinct prefixes, context compactions. We never apportioned the total among those causes, and the controlled replay further down is what identifies the mechanism.
The field Codex cannot emit
GPT-5.6 lets a caller mark the exact end of a reusable prefix by adding one field to a supported content block:
"prompt_cache_breakpoint": {"mode": "explicit"}
Codex does not omit this field by choice. It has no way to express it. In codex-rs/protocol/src/models.rs, the relevant InputText content variant carries exactly one field:
pub enum ContentItem {
InputText { text: String },
...
}
There is no #[serde(flatten)] and no extra-fields map, so the breakpoint has nowhere to live even internally. ResponsesApiRequest and ResponseCreateWsRequest both carry prompt_cache_key, and neither carries prompt_cache_options. Configuration does not reach it either: ModelProviderInfo customizes connection behavior rather than request bodies, and its schema is additionalProperties: false.
The most useful piece of evidence was already sitting inside the Codex repository. Codex vendors OpenAI’s own GPT-5.6 migration guide, which describes this failure mode directly:
GPT-5.6 implicit caching places a managed breakpoint near the latest user or tool message and no longer relies on 128-token rounding. A prompt with a large stable prefix followed by a changing suffix can therefore lose cache hits even when the stable prefix itself has not changed.
That paragraph describes Codex’s own request shape, ships inside Codex, and sits roughly thirty lines above a remedy Codex cannot apply and a cost warning explaining why the remedy now matters. A git grep for the field name across 5,744 tracked files returns exactly one hit, which is that documentation asset.
The replay that isolated the cause
The gateway processes each request rather than merely tunneling it, so we already had the exact bodies Codex had sent in production. Two controlled replays against live openai.gpt-5.6-sol — culminating in a 2×2 design that varied the breakpoint and cache key independently — identified the mechanism.
The first replay added the breakpoint to an otherwise unmodified body and pinned a single cache key across six requests. Hit rates went from 0% to 98.6%, and warm-request writes dropped from roughly 9,060 tokens to between 123 and 128. That result showed that the breakpoint worked when combined with a pinned key, but it does not describe a real workload, because Codex mints a fresh session UUID as its cache key every time it starts and never sends the pinned key that result relied on.
The second replay used three captured codex exec bodies and varied both changes independently.
| configuration | per-request hit rate | total cache-write tokens |
|---|---|---|
| Unmodified Codex | 0%, 0%, 0% | 16,873 |
| Explicit breakpoint only | 0%, 0%, 0% | 16,873 |
Stable prompt_cache_key only | 0%, 0%, 0% | 16,873 |
| Breakpoint and stable key | 0%, 96%, 97% | 5,973 (-65%) |
Neither change is sufficient on its own. The breakpoint defines which prefix is reusable, and the key determines whether subsequent requests can reuse it. Priced at the invoice rates, the same replay costs $0.1161 unmodified and $0.0471 with both changes applied, which is 59% less for the same captured workload. Write tokens fall further than spend does, because recovered tokens still bill as reads rather than becoming free.
Two caveats belong with that table. The three identical 16,873 figures are arithmetic rather than a copy-paste error: at a 0% hit rate every request writes its entire input, and replaying fixed bodies makes the total repeat exactly. And OpenAI documents prompt_cache_key as a routing hint combined with the prefix hash rather than a hard partition, so the requirement for a stable key is our measured observation on Bedrock Mantle rather than a universal server invariant.
What the cause was not
Four other explanations were tested against the live model and ruled out. A 101,448-token prefix hit 99% with a breakpoint present, ruling out a ceiling below that size in this test. Six concurrent requests with the same payload against a warm key all hit 99%; we did not reproduce, under this test, the write race reported in #33821. Streaming made no difference, with stream: true and false behaving identically. Reasoning effort made no difference either, with every level from low through xhigh showing the same 0% baseline.
What we could do without waiting for Codex
The defect is in a Rust type, so the definitive repair belongs in Codex. We cannot make that upstream repair ourselves, and configuration does not reach the field, so we looked for a mitigation while the Codex change is pending. The only layer we control is the hop between the agent and the model.
Codex talks to Bedrock through a gateway we built, which authenticates the developer, applies the spend cap and signs the upstream call. Doing that means parsing the JSON body and serializing it again, and a layer that already rewrites the body can add a field to it, including a field the client has no way to express. Our write-up on governing Claude Code, Claude Desktop and Codex behind one Bedrock gateway covers the rest of that design.
The mitigation is inject_prompt_cache_breakpoint(), which walks the outbound request, finds the last supported content block in the measured stable prefix, and marks it. It is guarded to GPT-5.6 model IDs and non-chat requests, defers to any client-supplied breakpoint, and skips block types that do not accept the field. On its own it would have changed nothing, so the gateway also sends a cache key that stays stable across independent sessions. Both changes are in production. In the controlled replay, together they produced 96% to 97% warm-request hit rates and reduced cache-write tokens by 65%. Account-level daily spend also fell after rollout, although these all-model totals do not isolate the mitigation’s effect.

Placement is harder than it first looks, and the same difficulty constrains any upstream fix. Codex ships use_responses_lite for all three GPT-5.6 slugs, which splices an initial additional_tools item onto the front of input with no content array at all. Automatic breakpoint placement therefore has to cope with a leading block that has nowhere to put one, and that is why #31882 is directly relevant here rather than incidental.
What still has to change in Codex
A documented GPT-5.6 API field remains unrepresentable in Codex’s wire types. Until openai/codex#35300 is resolved, Codex users on GPT-5.6 via Bedrock running one-shot invocations, forked subagents, or multi-agent workloads can keep paying the write premium on a prefix that never changed. A gateway mitigation only reaches traffic that crosses a gateway.
The most plausible-looking workaround does not reach the field. #34569 proposes per-provider top-level request-body passthrough, which could supply prompt_cache_options. A breakpoint is nested inside input, and that proposal explicitly must not override Codex-managed keys such as input.
The minimum upstream change is small: an optional breakpoint field on the supported content variants with skip_serializing_if = "Option::is_none", serialization tests proving it is emitted only where supported and omitted by default, and a capability gate. That gate should read capability metadata such as supports_prompt_cache_breakpoints rather than testing the model name, because older models reject these fields, and custom aliases and OpenAI-compatible backends do not map cleanly onto a check for 5.6.
How to check the same thing in your own account
In Cost Explorer, group the GPT-5.6 model service by usage type and compare cache-write tokens against cache-read tokens. A ratio far above 1 is a signal to investigate, not proof of this mechanism. Correlate it with gateway or request logs to determine whether codex exec in CI, forked subagents, or other automated workloads are the main contributors.
Four properties of the data are worth knowing before drawing conclusions from it.
RECORD_TYPE = Usageis what isolates metered consumption. Credits, refunds and other non-usage records net against usage in the default view, which is how a service with real spend can show $0.- Use closed days only. Cost Explorer marks in-progress periods
Estimated: true, and the same window re-read at 329.5M and then 375.2M cache-write tokens. The ratios held across pulls while the totals moved. - Dividing the accrued figures yields $5.4997/M input rather than $5.5000, which is rounding rather than a different rate. Taken from published rates, $6.88 / $5.50 is 1.25× and $0.55 / $5.50 is 0.10×, matching the documented prompt-cache multipliers and confirming these are the cache meters rather than generic input billing.
- The usage-type string carries the detail.
USE1-MP:USE1_cache_write_tokens_30m_standard-Unitsencodes the caching guide’s default 30-minute TTL, which identifies the meter provider-side.
For traffic that already crosses a gateway, inject the breakpoint and pin a cache key that survives across sessions, because, in the replay, either change alone produced a 0% hit rate. For traffic that does not, there is no configuration-only workaround today, and #35300 is the thing to follow.
Frequently asked questions
Why is cache-write spend so high for Codex with GPT-5.6 on Bedrock?
GPT-5.6 relies on an implicit managed breakpoint placed near the latest message. Codex cannot emit the explicit prompt_cache_breakpoint that would mark the end of its stable startup prefix, and it creates a new prompt_cache_key for every session. In affected workloads, independent requests that share the prefix but diverge at the first user turn can rewrite the whole prefix instead of reading it. Cache writes are billed at 1.25× uncached input, so each rewrite costs 12.5 times what the equivalent read would have cost.
Did this happen on GPT-5.5 too?
The client sent the same request shape, but under the rates applicable to GPT-5.5, cache writes incurred no separate charge. Over the same period, our GPT-5.5 usage recorded 9.17M input tokens.
Is this specific to Amazon Bedrock?
The serialization defect is backend-independent and verifiable directly from the Codex source. Our absolute cache-hit and billing measurements come from Bedrock Mantle and should be reproduced against other endpoints before being treated as universal, particularly the finding that a stable cache key was also required.
Can I fix it with configuration?
No. ModelProviderInfo customizes connection-level behavior rather than request bodies, and its schema is additionalProperties: false. Even a top-level body passthrough would not reach a breakpoint nested inside input.
Does a gateway fix it completely?
No. For traffic that passes through one, a gateway mitigates the issue by injecting the breakpoint and maintaining a stable cache key across independent sessions; in the replay, that reduced cache-write tokens by 65%. It does not eliminate cold starts or reach Codex traffic that bypasses it, so the upstream change is still needed.
Bottom line
Cache writes were 90% of our GPT-5.6 spend. The controlled replay isolated one mechanism behind the expensive rewrites, and the clue was already vendored inside Codex: a migration guide describing the client’s own failure mode next to a remedy its types cannot carry.
The mitigation is real and it is running, but it is bounded. A gateway can set a cache point that Codex cannot, and it can only do that for requests that cross it. In affected workloads that do not cross a gateway, Codex on GPT-5.6 can remain more expensive than necessary until the client can emit the field itself.
Related reading: Codex and OpenAI Agents on Amazon Bedrock, governing Claude Code, Desktop, and Codex behind one Bedrock gateway, and Claude Code on AWS.





