~/blogtoken-savings-cache-rewrite.md
cchu@nycu:~/blog$ cat token-savings-cache-rewrite.md
2026.10.246 min[agents][prompt-caching][compression][testing]

A Smaller Prompt Can Cost More After a Cache Rewrite

A Headroom cost-policy replay shows when token reduction loses to prefix-cache invalidation, and how a batch gate avoids charging the same suffix twice.

Removing 2,000 tokens looks useful until the edit sits before a warm 50,000-token suffix. Under Headroom's default cost-model inputs, that mutation loses 55,500 plain-input-token cost units. The same reduction with a cold cache gains 4,300 units.

These are executed synthetic calculations, not measured bills. The useful finding is the decision boundary: a compression layer needs to account for the cache it invalidates, alongside the tokens it removes. Headroom has an explicit gate for that tradeoff, including a batch rule that charges a shared invalidation only once.

I examined revision e995eb9f1dacac7ed548758406de6c1508caef95, whose package configuration declares headroom-ai version 0.41.0. I ran its Python cost policy and an isolated router method with Python 3.12.14 on Linux. I did not run its Rust compressor, tokenizer, proxy server, or provider requests. The token counts, cache state, and expected reuse below are fixture inputs.

An unchanged prefix remains reusable. Compressing an earlier tool result changes the prefix for the later suffix. A deeper edit can share that invalidation after the earlier edit has been admitted.

A Compression Candidate Has a Position

Headroom contains a native compression core, Python integration layers, provider adapters, and tests for routing and cache behavior. Its build configuration uses Maturin; this is more than a wrapper around a summarization prompt. For example, the Python diff-compressor interface delegates execution to the native core.

My subject is narrower: the economic decision after a candidate's original and compressed token counts are available. The Python cost policy mirrors a canonical Rust policy. I inspected both implementations and their tests; runtime results here come from Python, not a cross-language parity run.

The model distinguishes four inputs:

InputMeaning in the examined code
delta_tTokens removed from the candidate message
suffix_tokensToken total in messages after that slot
expected_readsModeled remaining reuse count, R
p_aliveEstimated probability that the existing cache is still useful

Write and read prices are relative to an ordinary input token. The code's five-minute tier uses w = 1.25 and r = 0.1; its one-hour tier uses w = 2.0. These are constants in the pinned model, not a fresh pricing survey or a dollar-denominated benchmark.

The Warm Case Changes the Arithmetic

The general gain expression is:

gain = delta_t * (w + r * (R - 1))
       - p_alive * (w - r) * (S + delta_t)

Here S is the suffix token count. With a cold cache, removing tokens avoids their initial write and subsequent reads. With a warm cache, those removed tokens have already been written. Keeping them would require reads; changing the earlier message also makes the later suffix need another write instead of a read.

Setting p_alive = 1 reduces the expression to:

gain = delta_t * r * R - (w - r) * S

That reduction explains the S + delta_t penalty in the general formula. Charging only S there would incorrectly count a fresh write saving for tokens that were already cached.

The policy admits a mutation only when gain is strictly positive. Its warm-cache break-even helper returns:

R = ((w - r) / r) * (S / delta_t)

For a 2,000-token cut before a 50,000-token suffix, the computed boundary is 287.5 at the five-minute write tier and 475.0 at the one-hour tier. These are model boundaries, not forecasts of how long an agent session will run.

Executed Cases

The replay probe verifies SHA-256 digests of four pinned source files. It loads the unchanged policy modules through isolated package namespaces, bypassing SDK initializers. For the router gate, it selects the original _net_cost_allows() method and its log-bucket helper from the source AST, compiles their unchanged bodies, and supplies the real policy on a small receiver object.

This isolates the decision logic from native compression and provider dependencies. It does not test importing the complete SDK, actual token counting, request dispatch, or the surrounding router workflow. Obtain the four files listed in the probe from the pinned revision, preserving their paths, to replay it.

From the portfolio checkout, I ran:

python --version
python public/blog/token-savings-cache-rewrite/cost-probe.py /tmp/headroom-note24

Both commands exited zero. The five cost cases used R = 10:

Tokens removedSuffix tokensCache aliveWrite tierComputed gainDecision
2,00050,0001Five-minute-55,500Skip
2,00050,0000Five-minute4,300Mutate
50,00010,0001Five-minute38,500Mutate
2,0001,5001Five-minute275Mutate
2,0001,5001One-hour-850Skip

All gains use plain-input-token cost units. The last two rows hold the candidate and reuse assumptions fixed: changing only the write tier reverses the decision.

Selected stdout:

small-warm: gain=-55500.0; mutate=False
small-cold: gain=4300.0; mutate=True
large-warm: gain=38500.0; mutate=True
near-5m: gain=275.0; mutate=True
near-1h: gain=-850.0; mutate=False
small-warm break-even, ttl=300: R=287.5
small-warm break-even, ttl=3600: R=475.0
PASS: 5 cost cases, 2 break-even boundaries, 4 isolated gate scenarios

A Batch Must Remember What It Already Invalidated

Evaluating every edit independently would overcharge overlapping suffixes. In the examined router gate, batch_state["floor"] records the shallowest admitted full-suffix mutation. A candidate at a strictly deeper slot then uses S = 0 in its incremental calculation: the earlier edit has already paid for invalidating that region.

The isolated replay checked four scenarios:

Gate scenarioObserved result
Small warm-cache edit rejectedBatch floor stayed unset
Large shallow edit admitted, followed by a small deeper editBoth admitted; deeper edit recorded router:netcost_batch_admit
Earlier edit evaluated with only a deeper floorEarlier edit still rejected
Small edit with an explicit p_alive_override = 0Admitted with router:netcost_idle_compaction

The floor therefore represents an admitted change, not merely a candidate that the compressor considered. Its direction matters too: invalidating a later prefix does not subsidize an earlier edit.

The surrounding apply() code enables this gate only when HEADROOM_NET_COST_POLICY=1. It builds suffix totals, prefers a request-level cache TTL, and can derive cache survival as max(0, 1 - idle_seconds / ttl). Those integration details are code inspection. My probe calls the gate directly; it does not establish that a deployed proxy enables it or forwards those inputs correctly. The upstream integration tests exercise broader routing behavior, but I did not execute that suite.

The remaining uncertainty is operational. Expected reuse and cache survival are estimates, and the suffix size depends on the tokenizer and request adapter. A linear idle-time estimate does not observe the provider's cache. Compression quality is another independent constraint: a positive modeled gain says nothing about whether the compressed tool result retained the evidence an agent needs. This gate answers a useful, smaller question—whether a proposed token saving is worth the cache rewrite under explicit assumptions.