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

Status Open
Maintainer reply None cached
Activity 3 comments · opened Jul 5, 2026
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 ↗

3 Comments

kcarriedo · 1 month ago

This is one of the most rigorous cost analyses I've seen filed against claude-code. The counter-intuitive result -- that blanket 1h TTL makes things more expensive, not less -- is worth highlighting since "just give them 1h" is the obvious wrong answer.

The retention-on-spawn fix is the cleanest lever here. The parent cache dying during exactly the window it's blocked on a child (and can't self-refresh) is a structural gap, and the 5-minute cliff at median 9-minute child runtime means it's hitting on the majority of real subagent waits, not just edge cases.

One implementation detail worth raising: the "write 1h TTL on dispatch" behavior has to happen in the harness, not the subagent, because the parent needs to set it before yielding control. The harness knows it's about to block; the subagent doesn't know when the parent will get called next.

For anyone reproducing this: the estimator script linked in the issue works on local transcripts with no credentials needed. I ran it on a similar workload and the measured vs modeled numbers are close enough to trust the directional estimates.

ZeroThaHero · 1 month ago

Filed #81389 as an adjacent case; cross-linked instead of piggybacking, since it's a different body of tokens.

Yours hits the prefix that's identical across siblings of a type. Mine's the parent-accumulated stuff (repo context the parent already prefilled, then re-pasted into every child brief). Task-specific, not per-type, so TTL and reordering do not touch it. Your fix 2 looks like where it would generalize from.

Enominera · 1 month ago

Strong +1 on this. Running a production OpenClaw deployment with 38 cron tasks, I measured the actual impact:

  • 77.4% overall cache hit rate — but almost entirely from manual interactive sessions (same-session prefix reuse)
  • Cron tasks: ~0% cache hit — each run creates a new session, and the static prefix (CLAUDE.md + rules + skills, ~30K tokens) is rewritten every time
  • Daily waste: ~672 cron requests × ~30K uncached static tokens = ~20M tokens/day of pure waste

Your Fix 3 (reorder dynamic content after the prefix breakpoint) is the highest-leverage change. The current implementation puts cache_control only on the last user message block — which is the one block that changes every request. Meanwhile, the stable system prompt blocks (skills, CLAUDE.md, memory) have no breakpoints at all.

Also confirmed via traffic inspection that #47098 is the same root cause. Both issues describe the same architectural gap.