[BUG] Prompt Cache Invalidation on Session Resume: Tool-Use Content Not Cached, Plugin State Changes Cause Full User Content Rewrite
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
Claude Code's prompt caching implementation fails to cache tool-use content (Read operations) across session resumes, and plugin state changes during active sessions trigger complete rewriting of cached user content. This particularly impacts users employing context-window optimization strategies (frequent plugin enable/disable to manage token costs).
Testing Environment
- All tests conducted within 5-minute cache TTL window
- Operations: C+C to exit session →
claude --resume "sessionid"→ Read tool invocations - Resume intervals: 5 seconds to 3 minutes (no variance in behavior)
Observed Behavior
Tool-Use Content Caching Failure
Cache metrics across various transcript compositions (within 5min TTL):
| Transcript Composition | Tool Reads | Before Resume | After Resume | Delta | Hit Rate Change |
|---|---|---|---|---|---|
| Baseline | 0 | Cache: 60K | Write: 2-3K, Read: 60K | +2-3K | 99% → 96% |
| Minimal reads | 2-3 | Cache: 65K | Write: 2K, Read: 65K | +2K | 99% → 96% |
| Moderate reads | 5-7 | Cache: 70K | Write: 4-5K, Read: 60K | +4-5K | 99% → 92% |
| Heavy reads | 20+ | Cache: 117K | Write: 91K, Read: 19K | +91K | 99% → 17% |
Critical observation: Cache reads decrease while writes increase. In heavy-read sessions, 98K of previously-cached content is not retrieved; instead, 91K of new tokens are written to cache, indicating complete re-evaluation/rewriting of tool-use content section.
Plugin State Changes Trigger Full User Content Cache Rewrite
When plugin state changed mid-session (enable/disable via /plugin command), systematic cache invalidation was observed across multiple test cycles:
Plugin Enable (Disable → Enable)
| Test | Initial Cache Read | Final Cache Read | Cache Write | Invalidated | Preserved |
|---|---|---|---|---|---|
| Enable Test 1 | 113K | 0K | 115K | 113K | 0K |
| Enable Test 2 | 123K | 19K | 105K | 104K | 19K |
Pattern: Enable operations wipe cache (sometimes completely to 0K, sometimes to 19K baseline), then rewrite 100-115K of user content.
Plugin Disable (Enable → Disable)
| Test | Initial Cache Read | Final Cache Read | Cache Write | Invalidated | Preserved |
|---|---|---|---|---|---|
| Disable Test 1 | 115K | 19K | 97K | 96K | 19K |
| Disable Test 2 | 122K | 19K | 103K | 103K | 19K |
Pattern: Disable operations consistently preserve 19K (system context only) and rewrite 96-103K of user content.
Critical Finding: System Context Preservation
19K preserved = System Prompt (3.6K) + System Tools (~15.3K) — only system context survives plugin state changes; all user content and tool-use is rewritten.
System Tools Size Change Does Not Correlate
- Plugins disabled: System tools: 17K
- Plugins enabled: System tools: 16.4K (smaller, yet cache still invalidated)
Implication: Cache invalidation is not driven by system tools size changes; the issue involves content differences or cache validation key changes in plugin definitions, not simple size deltas.
Asymmetric Behavior Confirms Bug
Enable and disable handle cache invalidation differently, with enable showing more aggressive cache wiping, indicating a bug in cache validation logic during plugin state transitions.
Technical Analysis
What Remains Stable (Not Invalidating Cache)
- System prompt hash
- System tools definitions
- Session duration between closes (5s to 3min: identical behavior)
- Memory files (insufficient test data, but statistically stable)
What Invalidates Cache
- Tool-use content sections (Read tool outputs) — proportional to read count
- Plugin state changes — complete user content cache rewrite
- System prompt regeneration — affects all downstream cache validation
Root Cause Hypothesis
Tool-use content (transcript sections containing Read tool responses) either:
- Is excluded from prompt caching entirely, or
- Uses a different cache validation key that differs between resumes, or
- Is included in a cache segment that is invalidated by system prompt changes
Plugin state changes likely trigger a system context rebuild that cascades to user content re-evaluation.
Impact Assessment
Use Case 1: Frequent Session Exits/Resumes Within TTL
- Likelihood: Low (most users don't exit/resume frequently within 5min window)
- Impact: 2-91K token waste per resume depending on session size
- Severity: Moderate (edge case, but compounding for heavy CLI users)
Use Case 2: Context Optimization via Plugin Management
- Likelihood: HIGH — users managing context windows to reduce costs frequently enable/disable plugins mid-session
- Impact: Complete user content cache rewrite on every plugin state change
- Severity: CRITICAL — affects all users employing cost-optimization strategies
Test Data Summary
Session Resume Tests (within 5-minute TTL)
- Total resume tests: 15+ sessions
- Small transcripts (0-7 reads): Consistent 2-5K write overhead
- Large transcripts (20+ reads): 91K write, 17% hit rate (complete tool-use invalidation)
- Resume timing: 5 seconds to 3 minutes — no correlation with invalidation pattern
Plugin State Change Tests
- Total plugin state transitions tested: 4 cycles (2 enable, 2 disable)
- Plugin enable: 100-115K user content rewrite (0-113K previously cached dropped)
- Plugin disable: 96-103K user content rewrite (19K system context always preserved)
- System tools size variance: 17K (disabled) vs 16.4K (enabled) — size change did NOT prevent cache invalidation
- Asymmetric behavior: Enable and disable handle cache invalidation differently, confirming implementation bug
Key Findings
- Tool-use (Read operations) not cached — proportional to large content rewrite on resume
- Plugin state changes trigger catastrophic cache loss — affects every user managing context
- System context (19K) preserved on disable only — enable operation completely wipes cache before rebuilding
- Size-based cache invalidation hypothesis rejected — system tools size decreased but cache still invalidated
#What Should Happen?
Expected Behavior
- Tool-use content cached in initial session should be reused without modification on resume
- Plugin state changes should not invalidate cached user content (only system context)
- Cache hit rates should remain >95% on resume regardless of tool-use volume or plugin changes
Workaround
None identified. Users cannot prevent plugin-triggered cache invalidation without abandoning mid-session plugin management.
Steps to Reproduce
# Session 1: Build transcript with tool use
claude --resume "sessionid" # or new session
# Execute: read file1.txt, read file2.txt, ... (multiple reads)
# Execute: /plugin enable some-plugin (or disable)
# C+C exit
# Session 2: Observe cache metrics
claude --resume "sessionid"
# Observe: Cache Write increases significantly, Cache Read decreases, Hit Rate drops
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.49 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Windows Terminal
Additional Information
_No response_
12 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
The problem still occurs in Claude Code 2.1.71
The problem still occurs in Claude Code 2.1.81
Also getting this with llama.cpp and Qwen 3.5 27B:
I've done extensive analysis on this exact problem. Using ccusage_go (open-source Claude Code usage tracker), I found that Cache Read tokens consumed 97.7% of my session costs — API actual cost was $1.47, total billed cost was $64.98 (a 44x markup). Cache also degrades instruction following in long sessions, which I documented with per-turn JSONL analysis.
Full write-up with data, community issue references, and Claude Code's own self-analysis report:
https://blog.sd.idv.tw/en/posts/2026-03-25_claude-code-cache-trap/
Tool: https://github.com/SDpower/ccusage_go
I'm getting the same issue. If I step away for more than 10 mins then come back, the first message (even before any work is done) uses 10-20% of my $100 max plan.
I'm not sure both of you mean the same issue I described. The issue I notices is an inconsistency in the caching behavior.
The cache holds for the most part when existing and resuming a session within the 5min cache window but there are irregular behaviors which shouldn't occur:
The two issues from you two seem like a misunderstanding in how the cache and the conversation work. On each message, the entire conversation history is sent back to the model and the whole conversation is processed to calculate the next response. If this was done without any optimization the compute time and cost would be a getting more with each added message. That's why this process is cached to reduce time and compute which is the reason why cache read tokens will be most of the token usage and also why these tokens are "cheaper" than the input, cache write and output tokens. By default the cache is a rolling 5 minute window which is invalidated 5 minutes after the last response from the user/model. On your next message after the cache is invalidated the conversation is written to the cache again which results in cache write tokens instead of the cache read tokens.
Hope this rough explanation helped and if one of you meant something different then it might help to explain it in longer format here or if it is not related to this issue in a new issue by itself.
Both issues described here are addressed by our cache-fix interceptor (
npm install -g claude-code-cache-fix, v1.2.0).Tool-use content not cached across resume — This is caused by attachment blocks (skills, MCP, deferred tools, hooks) drifting to the wrong position in the message array on resume, breaking the prompt cache prefix match. The interceptor scans the full message array on every API call, relocates the latest version of each block type back to
messages[0]in a deterministic order, and stabilizes thecc_versionfingerprint in the attribution header. This preserves the prefix across resume.Plugin state changes triggering full rewrite — Plugin enable/disable changes the skills and deferred tools blocks, which changes the prefix bytes. The interceptor normalizes these blocks on every call: skills entries are sorted alphabetically, tool definitions are sorted by name, and ephemeral content (session_knowledge) is stripped. This makes plugin state changes invisible to the cache prefix.
Eliminating the resume cold start entirely — Even with the relocation fix, the first post-resume API call used to trigger a cache rebuild because CC reassembles messages differently on resume. As of v1.2.0, the interceptor includes a prefix lock feature (
CACHE_FIX_PREFIX_LOCK=1) that saves the exactmessages[0]content and replays it on resume, producing a byte-identical prefix. In our testing, this achieved 99.4% cache hit rate on the first post-resume call vs 44% without.Your heavy-read scenario (20+ Read ops, 99% → 17% hit rate, 91k rewrite) is the exact pattern the relocation fix prevents. Your plugin toggle scenario (113k invalidated) is what the block sorting and tool stabilization prevent.
The interceptor works as a Node.js preload module — requires the npm installation of Claude Code, not the standalone binary.
I read through it and yes that might help with the drifting messages on resume if these are the cause but I'm unsure how sorting the tools and skills by names would actually fix the plugin state changes. It might improve the cache hit rate because drift in the order is avoided but the plugin tool registration is logically wrong in Claude Code. The registered plugin tools are always at the conversation start and there is no intermediate message logic informing the model of changed registrations but instead the conversation is rewritten by modifying the changed plugin tools at the start.
If your interceptor addresses that too and I missed it let me know because I would be interested how you solved that.
Other than that your interceptor is a nice idea but I would prefer if the Claude Code team would actually address these issues in their code instead of adding a wrapper around the API calls. These harnesses like Claude Code change so quickly that adding more external complexity might only be helpful for one or two versions or prevent internal fixes from working.
Good questions. Let me address both.
Plugin tool registration: You're right that the issue goes deeper than just ordering — CC rewrites
messages[0]with modified plugin/MCP blocks but doesn't inform the model via intermediate messages. Our interceptor does handle this. ThenormalizeResumeMessagesfunction scans the entire message array backward on every API call to find the latest version of each block type (skills, MCP, deferred tools, hooks), removes all instances from wherever they've drifted, and consolidates them intomessages[0]in a deterministic order (deferred → mcp → skills → hooks) matching fresh session layout. So when CC rewrites a plugin registration into a later message position, we catch it and move it back. Skills entries and tool definitions are also sorted alphabetically so that enable/disable toggling doesn't change prefix bytes.It doesn't fix the logical problem you're describing (the model not being told about registration changes via intermediate messages) — that's an architectural issue CC needs to address. But it does prevent the cache bust that results from the rewrite.
Fragility concern: Completely agree. This is a stopgap, not a solution. We'd much rather see Anthropic fix the root causes in CC itself. The interceptor exists because these bugs have persisted across multiple versions (confirmed through v2.1.92) and the cache cost impact is significant — our performance data shows 52–73% of API calls need block relocation and 98% need tool reordering. That's a lot of unnecessary cache rebuilds on every resumed session.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.