[FEATURE] Subagent prompt-cache strategy inflates prompt spend ~14% — 3 structural fixes (measured)

Open 💬 1 comment Opened Jul 5, 2026 by marcoabreu
Preflight: I searched existing issues/requests and didn't find this. This is one coherent enhancement — how subagents are prompt-cached — whose single root cause has three related sub-changes; I've kept them together because they interact (fix 3 depends on fix 2, and the "obvious" one-line fixes backfire in isolation).

TL;DR

I profiled prompt-cache usage from my own local Claude Code transcripts — ~95 sessions, ~1,800 subagents, 6.8B input tokens over ~2 weeks of heavy multi-agent use. Overall caching is healthy (~95% of input tokens are cache hits). But the way subagents are cached means the same tokens get billed at write rates instead of read rates — it doesn't process more tokens, it just costs more. In my data:

  • Subagent prompt cost could drop ~14% (input-side).
  • Overall spend could drop ~8% (whole thing, including the main session and output, which caching can't help).

The intuitive fix — "let subagents use the 1-hour cache like the main loop" — makes it 8.6% more expensive. So does a naive volatility split (+1.3%). The wins are structural and use only today's GA prompt-caching API (no new features):

| Change | Δ subagent prompt cost (per token) |
|---|---|
| ❌ Naive: subagents → blanket 1h TTL | +8.6% (worse) |
| ❌ Naive: head-1h / tail-5m volatility split | +1.3% (worse) |
| ✅ Retention-on-spawn (1h TTL on the write before a child dispatch) | −6.0% |
| ✅ Persistent per-type prefix (alone) | −1.0% |
| ✅ Reorder dynamic content after the prefix breakpoint | −7.6% |
| ✅ All three combined | −13.6% |

!what each cache change does to subagent cost

The per-lever figures above are per-token cost (model-agnostic read/write multipliers). Applied to my actual model mix, the combined effect is ~14% of subagent prompt spend / ~8% of total — the script at the bottom computes both for your own data.

Check your own: the estimator script below reads your local transcripts and prints your two numbers. This only matters for subagent-heavy workflows — if you don't fan out to subagents, you won't see it.

---

Method (reproducible)

Claude Code stores every API call's usage in local transcripts:

  • Main loop: ~/.claude/projects/<proj>/<session>.jsonl
  • Subagents: ~/.claude/projects/<proj>/<session>/subagents/agent-*.jsonl + a .meta.json carrying agentType and spawnDepth

Each assistant record has message.usage: input_tokens (uncached, 1.0×), cache_creation_input_tokens (writes), cache_read_input_tokens (hits, 0.1×), and a cache_creation split into ephemeral_5m_input_tokens (1.25×) and ephemeral_1h_input_tokens (2.0×). Streaming writes each turn ~3×, so dedupe by message.id. A child's meta.toolUseId equals the Agent/Task tool-use id in exactly one parent transcript, so you can correlate spawns with the parent's cache behavior and measure the unique task prompt.

---

What's happening today

Subagents do cache — but every subagent write is ephemeral_5m: subagents are on the 5-minute TTL, while the main loop uses 1-hour. Reads refresh the TTL for free, so an active agent never loses its cache; expiry happens only during an idle gap with no reads — i.e. exactly when a parent is blocked waiting on a child. That creates two distinct problems.

Problem 1 — a parent's cache dies while it's blocked on a child

When a parent spawns a child and waits, no reads refresh the parent for the child's whole runtime. Past 5 minutes, the parent's cache expires and its entire context is re-written on the next turn. Measured: hundreds of these, 96% true cache deaths (the re-write covers ≥50% of the previously cached prefix), at a median idle gap of ~9 minutes — just past the 5-minute cliff.

The main loop doesn't have this problem because it's on 1-hour. (I also simulated giving the main loop a shorter-TTL tail: it's a −22% net loss, because the main loop's large context frequently idles 5–60 min while orchestrating subagents — so its 1-hour TTL is correct. That's the whole point: the main loop already has the long retention subagents need and lack, even though subagents wait on children too.)

Problem 2 — every subagent re-sends ~30k tokens of identical static context

A subagent's cold-start context is ~37k tokens, of which only ~950 (≈3%) is the unique task. The other ~97% is static — system prompt, tool schemas, project rules, environment — near-identical across all subagents of the same type, yet mostly re-written from scratch on nearly every spawn.

!cold-start is ~97% static

(Of that ~97% static, roughly 80% of the whole cold start is re-written from scratch every spawn and only ~16% is served from cache — the chart's red vs green.)

Cross-subagent sharing does work when it can: a subagent whose same-type sibling ran within the last 5 minutes hits the shared prefix 85% of the time; with no recent sibling it drops to 45%. Frequently-used types benefit; sporadic ones (specialized reviewers fired >5 min apart) re-write their entire ~30k static prefix cold, every time. Two causes: the 5-minute TTL, and per-invocation dynamic content (date, cwd, git branch, injected reminders) positioned early enough to invalidate the otherwise-identical prefix (the docs' tools → system → messages hierarchy invalidates a level and everything after it).

---

Why the "obvious" fixes cost more

  • Blanket 1h for subagents: +8.6%. 98% of cache reuse happens within ~34 seconds (median gap 7s), so 5-minute retention already covers almost everything. Paying the 2.0× (vs 1.25×) write premium on every write — cold-starts and incremental deltas alike — to rescue the ~2% of accesses that cross 5 minutes is a net loss.
  • Head-1h / tail-5m volatility split: +1.3%. The volatile tail still dies during a long child wait and gets re-written anyway, while you pay a universal 1h premium on the stable head.

The TTL value is not the lever. The question is which specific content gets ttl:"1h" vs the default ttl:"5m", and when.

Which TTL, when

  • ttl:"1h" — use it in exactly two places:
  1. The per-agent-type static prefix (system prompt + tool schemas + static project rules). It's identical across siblings and read repeatedly over a long window; 1h keeps it warm for sporadically-spawned types.
  2. The parent's context breakpoint on the turn it dispatches a child and blocks. The parent will sit idle (no refresh) for the child's runtime, which routinely exceeds 5 minutes.
  • ttl:"5m" (default) — everything else, especially the churning conversation tail during active iteration. It's re-read within seconds, so 5m refreshes keep it warm; paying 1h's 2× write premium here is pure loss.
  • Rule of thumb: choose 1h only where the next read will come after a >5-minute idle gap, or where content is shared across many spawns over a long window. Never blanket-1h everything (that's the +8.6% mistake).

---

The three fixes

Fix 1 — Retention-on-spawn (−6.0%)

When the harness is about to dispatch a child subagent and block, write the parent's context breakpoint with cache_control:{ttl:"1h"} instead of the default 5m, sized to the expected child runtime. The parent's full context survives exactly the gap that would otherwise kill it, and it's read on the child's return instead of re-written. The 1h premium lands only on spawn-adjacent writes, not globally.

Fix 2 — Persistent per-type prefix (−1.0% alone, enables Fix 3)

Write each agent type's static prefix (system prompt + tool schemas) with ttl:"1h" and share it across siblings, so a sporadically-used type doesn't depend on a sibling having run in the last 5 minutes. Small alone, because today only a fraction of the static prefix is in a cacheable position — which is why you also need:

Fix 3 — Reorder dynamic content after the prefix breakpoint (−7.6%, with Fix 2)

Put the entire stable, shareable block (harness preamble, agent-type system prompt, tools, static rules) first, behind the 1h breakpoint, and move all per-invocation dynamic content (date, cwd, git branch, injected reminders, the unique task) strictly after it. This exposes the full ~30k static prefix as a shared read instead of a fresh write. Effect: cold-start becomes ~88% cheaper per spawn. Even if only half the static prefix is genuinely reshareable, this is still ~−3.6%.

Combined: −13.6% of subagent prompt cost (per token). Weighted by my model mix that's ~14% of subagent prompt spend and ≈8% of total — the main session and output are unaffected.

---

Check your own numbers

Self-contained, stdlib-only, reads local transcripts, prints aggregate numbers only, sends nothing:

python3 estimate_cache_savings.py        # defaults to ~/.claude/projects

Output on my machine:

scanned: 95 sessions · 1777 subagents · 6.82B input tokens
  >> Overall spend you could cut:       7.8%   ($5,075 -> $4,680 est.)
  >> Subagent prompt-cost efficiency:  14.3%   ($2,768 -> $2,373 on subagent input)

Script: https://gist.github.com/marcoabreu/b7b4522a8f508389d14ef962b310e927 (prices are list-price estimates you can edit at the top).

---

Feasibility (verified against the prompt-caching docs)

All three fit today's GA API — no beta header:

  • Mixing 1h + 5m breakpoints in one request is supported and GA, with the constraint that 1h breakpoints appear before 5m ones — the natural order (stable head first, volatile tail last). Fixes 1 and 3 rely on exactly this.
  • Up to 4 breakpoints per request.
  • Reads refresh the TTL for free, so retention costs only the one write.
  • Hierarchy invalidation (tools → system → messages) is precisely why early dynamic content poisons the shareable prefix — Fix 3 just moves it after the breakpoint.
  • Multipliers: read 0.1×, 5m write 1.25×, 1h write 2.0×.

Caveats

  • One developer, one broad codebase, ~2 weeks — directional, not fleet-representative. Magnitude scales with how heavily you use subagents.
  • "Subagents are on 5m" and the 97%/3% static/unique split are directly measured; the savings are modeled from the usage fields, not Anthropic's billing.
  • Load-bearing assumption is Fix 3's reshareable-static fraction — hence the sensitivity band (−7.6% at 100%, −3.6% at 50%).
  • $ figures use editable list-price estimates; on a subscription this is quota, not dollars.

Asks

  1. Give subagents retention-on-spawn: ttl:"1h" on the pre-dispatch write, not a blanket TTL.
  2. Persist the per-type static prefix (ttl:"1h", shared) so cross-subagent sharing doesn't depend on a 5-minute coincidence.
  3. Order subagent prompts so the identical static prefix is fully cacheable and per-invocation dynamic content sits after the breakpoint.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗