[BUG] Prompt Cache Invalidation on Session Resume: Tool-Use Content Not Cached, Plugin State Changes Cause Full User Content Rewrite

Status Closed — not planned
Reported on v2.1.49
Maintainer reply None cached
Activity 12 comments · opened Feb 20, 2026 · closed May 22, 2026

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

  1. Tool-use content sections (Read tool outputs) — proportional to read count
  2. Plugin state changes — complete user content cache rewrite
  3. 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

  1. Tool-use (Read operations) not cached — proportional to large content rewrite on resume
  2. Plugin state changes trigger catastrophic cache loss — affects every user managing context
  3. System context (19K) preserved on disable only — enable operation completely wipes cache before rebuilding
  4. 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_

View original on GitHub ↗

12 Comments

github-actions[bot] · 6 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/24121
  2. https://github.com/anthropics/claude-code/issues/26330
  3. https://github.com/anthropics/claude-code/issues/22107

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

thoeltig · 5 months ago

The problem still occurs in Claude Code 2.1.71

thoeltig · 5 months ago

The problem still occurs in Claude Code 2.1.81

alexellis · 5 months ago

Also getting this with llama.cpp and Qwen 3.5 27B:

slot update_slots: id  0 | task 1810 | erased invalidated context checkpoint (pos_min = 24270, pos_max = 24270, n_tokens = 24271, n_swa = 1, size = 149.626 MiB)
slot update_slots: id  0 | task 1810 | erased invalidated context checkpoint (pos_min = 27150, pos_max = 27150, n_tokens = 27151, n_swa = 1, size = 149.626 MiB)
slot update_slots: id  0 | task 1810 | erased invalidated context checkpoint (pos_min = 27662, pos_max = 27662, n_tokens = 27663, n_swa = 1, size = 149.626 MiB)
slot update_slots: id  0 | task 1810 | erased invalidated context checkpoint (pos_min = 29952, pos_max = 29952, n_tokens = 29953, n_swa = 1, size = 149.626 MiB)
slot update_slots: id  0 | task 1810 | erased invalidated context checkpoint (pos_min = 30464, pos_max = 30464, n_tokens = 30465, n_swa = 1, size = 149.626 MiB)
slot update_slots: id  0 | task 1810 | erased invalidated context checkpoint (pos_min = 30984, pos_max = 30984, n_tokens = 30985, n_swa = 1, size = 149.626 MiB)
slot update_slots: id  0 | task 1810 | erased invalidated context checkpoint (pos_min = 31496, pos_max = 31496, n_tokens = 31497, n_swa = 1, size = 149.626 MiB)
SDpower · 5 months ago

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

JacquardCauseway · 5 months ago

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.

thoeltig · 5 months ago

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:

  1. On resume there is always a small token count which should be cached read tokens but are added as new write cache tokens. This is most likely due to a timestamp, version or other variable which sometimes chances and rewrites part of the cache. The portion increase with the conversation length and sometimes the portion randomly is the entire conversation after the system prompt, Claude.md and system tools. Maybe this has also something to do with point 2 but I'm not sure about this.
  2. Changing plugin registrations mid conversation invalidates the cache which is a design decision by Anthropic. The available slash commands, agents and skills which are found in these plugins are added somewhere at the start of the conversation properly alongside the system tools. These are most likely modified through that which invalidates the whole conversation afterwards. The problem is that this is nowhere documented as far as I know and Claude Code supports /reload-plugins to load them without existing and resuming. This is bad for users trying to optimize the context and the model should be able to handle plugin changed if a "new available plugin tools" message would be appended at the reload/resume point in conversation.

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.

cnighswonger · 4 months ago

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 the cc_version fingerprint 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 exact messages[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.

thoeltig · 4 months ago

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.

cnighswonger · 4 months ago

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. The normalizeResumeMessages function 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 into messages[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.

github-actions[bot] · 3 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

github-actions[bot] · 1 month ago

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.