Prompt cache never hits across chained -p --resume calls, even at minimum config
Description
On chained -p --resume calls, prompt caching never accumulates. The static system-prompt/tool-definition prefix caches correctly (cache_read_input_tokens is nonzero and stable), but the per-turn conversation content never gets promoted into reusable cache after being written once — every subsequent call pays the same small fresh-write cost for new turn content instead of cache_read climbing to absorb what was created in the previous call.
To be precise about what's expected vs. not: it's normal for cache_creation_input_tokens to grow a little each call, since each --resume adds a genuinely new turn that didn't exist before. What's not expected is that cache_read_input_tokens stays completely flat forever instead of increasing turn-over-turn — the content written in call N should be readable from cache in call N+1, and it never is.
Reproduction
sid=""
for word in alpha beta gamma; do
if [ -z "$sid" ]; then
out=$(claude -p "Say the word $word" --model sonnet --effort high --output-format json 2>&1)
sid=$(printf '%s' "$out" | grep -oE '"session_id":"[^"]+"' | head -1 | cut -d'"' -f4)
else
out=$(claude -p "Say the word $word" --resume "$sid" --model sonnet --effort high --output-format json 2>&1)
fi
printf '%s' "$out" | grep -oE '"(cache_creation_input_tokens|cache_read_input_tokens)":[0-9]+'
echo "---"
done
Expected
cache_read_input_tokens climbs call-over-call as prior turns' content becomes reusable cache. cache_creation_input_tokens stays small and roughly constant (just the size of each new turn), not compounding.
Actual
cache_read_input_tokens never increases past its initial value, no matter how many resumed calls happen. cache_creation_input_tokens grows by a small but nonzero amount every call — consistent with each turn's own content, but never subsequently readable from cache on the next call.
Isolation work done
Bisected across 9 plugins, 91 skills, 48 remote connectors, 5 local MCP servers, 2 personal UserPromptSubmit hooks, and nested-session env vars (desktop-app child session vs. standalone terminal) — the flat-cache_read signature is identical at every configuration, including bare-minimum (no plugins/skills/MCP/connectors). Only the baseline token count scales with how much is loaded; the qualitative non-accumulation never changes. Also confirmed in a fully standalone Terminal.app session outside any parent Claude Code process, ruling out nested-session artifacts.
Environment
claude --version: 2.1.79 (Claude Code)- OS: macOS (Darwin)
- Invocation:
claude -p ... --resume <session_id>, one process per call, in a loop
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Update: confirmed this reproduces identically in a completely standalone Terminal.app session, fully outside any parent Claude Code/desktop process (no
CLAUDECODE,CLAUDE_CODE_CHILD_SESSION, etc. in the environment). Same signature:cache_read_input_tokensstuck flat across all 4 chained calls,cache_creation_input_tokensgrowing by a small but consistent amount every call instead of dropping to near-zero after the first.This rules out nested-session/desktop-app artifacts as the cause — it's present in the plainest possible invocation.
Also ran an exhaustive one-at-a-time bisection (documented above) across 9 plugins, 91 skills, 48 remote connectors, 5 local MCP servers, 2 personal UserPromptSubmit hooks, and nested-session env vars — every single toggle produces the identical flat-cache_read signature. Only the baseline token count scales with how much is loaded; the qualitative behavior never changes, including at bare-minimum config.
Given a colleague on what's described as the same account setup reportedly doesn't see this, it may be account/session-specific on the server side rather than something reproducible from client-side config alone. Happy to provide full debug logs if useful for triage.
Correction to the issue description above: tightened the framing. The bug isn't that the whole prompt rebuilds every call — the static system/tool prefix caches fine. The actual defect is narrower: per-turn conversation content never gets promoted into reusable cache after being written once, so
cache_read_input_tokensnever climbs turn-over-turn the way it should. Small but real, since it means no session ever benefits from compounding cache savings over a long conversation.Same shape reproduced on a different entry point: waking a named subagent with
SendMessage(Claude Code 2.1.260, macOS, 1-hour cache TTL, Haiku 4.5 subagent). Every wake reads the same ~22K static prefix from cache and re-writes everything after it, on every wake — not only the first one.| call | trigger | cache_read | cache_creation |
|---|---|---|---|
| 1 |
Agent(name: "probe-wake", …)spawn | 0 | 47,840 || 2 | same turn, after two tool results | 47,840 | 3,548 |
| 3 |
SendMessagewake, +10 s after idle | 22,312 | 29,308 || 4 |
SendMessagewake, +50 s after call 3 | 22,312 | 29,463 |Within a turn the prefix is reused (call 2). On each wake the cache hit stops at exactly the same offset and the remainder — the per-session injected context (CLAUDE.md, skill/agent listings) plus the whole conversation — is written again. The same session's main loop, at ~310K context, read a median 100% from cache across 38 user-turn requests (300–1,200 tokens written per turn), so this is specific to the resume/rebuild path.
Repro (no hooks returning
additionalContext, no MCP needed):Agent(subagent_type: "general-purpose", model: "haiku", name: "probe", prompt: "<run twocats of local files, end with a nonce line, call no tool afterwards>")SendMessage(to: "probe", message: "reply with nonce X")— twice, seconds apart~/.claude/projects/<project>/<session>/subagents/agent-*probe*.jsonl, comparemessage.usage.cache_read_input_tokens/cache_creation_input_tokensof the wake requests against the pre-wake request.Each wake also records
deferred_tools_deltaandskill_listingattachment entries (~10 KB) immediately before the request, which matches the mechanism #44045 captured for SDKresume(askill_listingblock inmessages[0]that differs between a fresh and a resumed session). #44724 reported theSendMessagepath in April with a different root cause (system identity string); both were closed as stale. This measurement suggests the partial-miss-on-every-resume variant is still present on the subagent wake path as of 2.1.260.