Session resume (--continue) invalidates entire prompt cache, causes massive rate limit consumption

Status Fixed / completed
Maintainer reply None cached
Activity 11 comments · opened Apr 2, 2026 · closed Apr 3, 2026

Summary

Resuming a session with --continue or /resume completely invalidates the prompt cache, even when re-entering within seconds. On a long session with a large prompt, this forces a full cache_creation of 400-500k tokens each time — silently burning through rate limits.

What happened

On a 1M context Opus 4.6 session with ~500k prompt, I exited and re-entered with --continue a few times. Each time, cache_read dropped to 0 and the entire prompt was re-cached from scratch — even though the re-entry happened within 2-3 seconds (well within 5-minute cache TTL).

Additionally, the prompt size barely changed after re-entry, meaning whatever restructuring happened during reload provided no benefit — it just destroyed the cache.

All resume methods affected (controlled tests)

Tested with the same session, varying only the resume method and flags:

Test 1: --continue --dangerously-skip-permissions (12s gap)
  Before: cr: 172,301  cc: 1,344
  After:  cr:       0  cc: 174,210  ← full re-cache

Test 2: --continue only (20s gap)
  Before: cr: 176,193  cc:     0
  After:  cr:       0  cc: 176,730  ← full re-cache

Test 3: /resume (interactive resume, ~2.5h gap)
  Before: cr: 160,149  cc:   314
  After:  cr:       0  cc: 156,682  ← full re-cache

All three: cache_read: 0, full re-cache. The --dangerously-skip-permissions flag is irrelevant — it's the resume/continue mechanism itself.

Raw Data (deduplicated by msg_id, from JSONL session log)

Spike 1: session resume after ~4h sleep (expected)

04:18:37  cr: 389,000  cc:     210  out:   640  stop:end_turn     ← last turn before sleep
         ... ~4 hours gap (cache TTL expired — this one is expected)
08:08:09  cr:       0  cc:       0  out:     0  stop:stop_sequence ← session resume
08:09:47  cr:       0  cc: 367,175  out:   822  stop:tool_use      ← 367k full re-cache (expected)

Spike 2: /exit → --continue, 2 seconds apart

09:04:15  cr: 499,517  cc:     486  out:   521  stop:end_turn      ← last turn before /exit
         ... /exit at 09:04:29, --continue at 09:04:31 (2 SECONDS)
09:04:31  cr:       0  cc:       0  out:     0  stop:stop_sequence ← session reload
09:05:13  cr:  26,691  cc: 470,381  out: 1,352  stop:tool_use      ← 470k re-cache! (WHY?)

Spike 3: /exit → --continue, 3 seconds apart

09:40:19  cr: 561,082  cc:     987  out:   289  stop:end_turn      ← last turn before /exit
         ... /exit at 09:40:21, --continue at 09:40:24 (3 SECONDS)
09:40:24  cr:       0  cc:       0  out:     0  stop:stop_sequence ← session reload
09:42:59  cr:  26,691  cc: 511,847  out: 1,649  stop:tool_use      ← 512k re-cache! (WHY?)

Messages in JSONL right before each spike

Spike 2 context:
  /exit command logged
  file-history-snapshot
  custom-title
  agent-name
  file-history-snapshot
  → stop_sequence "No response requested."
  → cr:0 cc:470,381

Spike 3 context:
  /exit command logged
  file-history-snapshot
  custom-title
  agent-name
  file-history-snapshot
  → stop_sequence "No response requested."
  → cr:0 cc:511,847

Before/after comparison

Before /exit  → After --continue (first turn)
prompt: 500k  → 497k (-0.6%)    cache_create: 470,381 (entire prompt re-cached)
prompt: 561k  → 538k (-4%)      cache_create: 511,847 (entire prompt re-cached)

Impact

In a 2.6-hour morning window:

  • 3 session resumes consumed ~1.43M cache_creation tokens
  • Actual useful output during same period: ~155k tokens
  • cache_creation was 9x the actual output
  • Hourly rate limit cost: 920k/hr vs normal 412k/hr (2.2x increase)

Root Cause

Per @ArkNill's analysis: the deferred_tools_delta feature introduced in v2.1.69 reorders tool results when reconstructing the conversation for resume, which breaks the cache prefix and forces a full re-cache. See #34629 and https://github.com/ArkNill/claude-code-cache-analysis.

Environment

  • Model: claude-opus-4-6 (1M context)
  • Platform: macOS
  • Multiple plugins/skills (~40k system prompt tokens)
  • Session duration: ~34 hours (long-running)

Related

  • #34629 — Prompt cache regression in --print --resume since v2.1.69
  • #40524 — Conversation history invalidated on subsequent turns
  • #42136 — Aggressive context compaction on 1M context
  • #38350 — Abnormal rate limit consumption

View original on GitHub ↗

11 Comments

ArkNill · 5 months ago

Your JSONL analysis confirms exactly what I've been tracking across multiple reports. The root cause is the deferred_tools_delta feature introduced in v2.1.69 — it reorders tool results when reconstructing the conversation for resume, which breaks the cache prefix and forces a full re-cache. This is the same mechanism behind #34629.

Your Spike 2 and Spike 3 data (2-3 second exit→resume gaps with 470-512K re-cache) are particularly damning evidence, since the 5-minute cache TTL should have trivially covered that interval.

Compounding factor worth noting

This interacts badly with thinking signatures (#42260). When resumed, base64-encoded thinking blocks from prior turns get replayed as input tokens. Combined with zero cache hits from the prefix break, a 400+ message session can easily send 500K+ tokens as fully-billed input on a single resumed turn — effectively a token bomb.

What I'd recommend until this is fixed

  • Avoid --resume / --continue entirely for any non-trivial session. Starting fresh with a well-structured CLAUDE.md to restore context is dramatically cheaper.
  • If you must track session state, export context to a file and feed it into a new session rather than resuming.
  • The cache_read_input_tokens field in your JSONL logs is the key diagnostic — any resume that shows near-zero cache read with high cache creation is hitting this bug.

I patched v2.1.81 to fix the cache prefix handling and verified cache read ratios returned to 89-99% on fresh sessions. The broader cache analysis (including this resume-specific issue) is documented at https://github.com/ArkNill/claude-code-cache-analysis.

taekim34 · 5 months ago

Thanks @ArkNill for the deferred_tools_delta root cause analysis and the link to your cache analysis repo. Very helpful!

I've updated the issue body with additional test results confirming /resume also triggers the same cache invalidation.

ArkNill · 5 months ago

Update (April 2): v2.1.90 fixes the main cache regression but this specific issue (--resume / session replay) remains. Avoid --resume entirely — fresh sessions with a structured CLAUDE.md are the workaround.

For non-resume usage, v2.1.90 now achieves 95-99% cache read on both npm and standalone. Details: https://github.com/ArkNill/claude-code-cache-analysis

simpolism · 5 months ago

2.1.90 has this line in the changelog:

Fixed --resume causing a full prompt-cache miss on the first request for users with deferred tools, MCP servers, or custom agents (regression since v2.1.69)

So I assume that was another bug? Because I cannot use the harnesses I built around claude -p --resume given the reported bug here. They eat my entire quota within a few turns.

ArkNill · 5 months ago

April 3 update: The cache regression (Bugs 1-2) is fixed in v2.1.91. However, systematic proxy testing revealed additional unfixed mechanisms — a 200K tool result budget cap, a client-side false rate limiter (151 synthetic entries found), and silent microcompact clearing (327 events). Anthropic responded on X (Lydia Hallie) acknowledging peak-hour tightening but stating "none were over-charging you" — our measured data shows mechanisms their statement does not cover. Full analysis: claude-code-cache-analysis

ArkNill · 5 months ago

Received a patch via email from @simpolism that addresses the remaining --resume cache misses on v2.1.91. Tested it locally through a monitoring proxy — results are solid.

The patch targets two additional root causes beyond what v2.1.90-91 fixed:

  1. Skill listing attachmentskill_listing (~375 tokens) isn't persisted in session storage. On resume it regenerates at a different position, changing the block structure and breaking the cache prefix.
  1. joinTextAtSeam newline accumulation — text-block merging function appends \n at boundaries during normalization. On replay, these accumulate, mutating the prefix each resume.

My test results on v2.1.91 (npm, patched, through cc-relay proxy):

| Turn | Type | Create | Read | Ratio |
|------|------|--------|------|-------|
| 1 | Initial | 3,963 | 25,576 | 86.6% |
| 3 | Resume 1 | 4,799 | 29,539 | 86.0% |
| 4 | Resume 2 | 89 | 34,338 | 99.7% |
| 5 | Resume 3 | 28 | 34,427 | 99.9% |
| 7 | Resume 4 (30s idle) | 36 | 36,964 | 99.9% |

From resume 2 onward: 28-89 tokens created per turn instead of tens of thousands. Context carries over correctly — the 4th resume accurately summarized the entire session history.

The patch needed a small adaptation for v2.1.91 (the original regex for joinTextAtSeam didn't match due to a literal newline in the minified template string — used brace-counting to find exact function boundaries instead).

Original gist: https://gist.github.com/simpolism/302621e661f462f3e78684d96bf307ba

For anyone using claude -p --resume in harness setups, this is worth looking at. For interactive use where you start fresh sessions, the impact is minimal since v2.1.90-91 already fixed the main cache regression.

simpolism · 5 months ago

@ArkNill thanks for posting it here -- I think there may still be a cache consistency issue around tool calls or file tracking across resumes. Hopefully can look into it shortly, having trouble reproducing the exact circumstances but got a pretty bad cache hit during a regular conversation on my patched version and will be keeping an eye out.

simpolism · 4 months ago

okay @ArkNill we found the remaining cache-breaking issues, they involved the TodoWrite and other injections happening before the cache prefix, invalidating the entire thing. I've updated the gist to a new version to deal with them as well. https://gist.github.com/simpolism/302621e661f462f3e78684d96bf307ba

full list of breaking attachments:

| Attachment | What it does | Why safe to suppress |
|---|---|---|
| skill_listing | Lists available slash commands | Interactive/IDE feature |
| todo_reminders | Periodic "use TodoWrite" nudges | Unnecessary in harness |
| unified_tasks | Task tracking state | Unnecessary in harness |
| changed_files | Diffs of previously-read files | Redundant (model sees tool results) |
| dynamic_skill | Scans skill directories | No dynamic skills in harness |
| nested_memory | One-shot memory triggers | Fires unpredictably |
| async_hook_responses | Intermittent hook feedback | No hooks in harness |
| diagnostics | LSP diagnostics | No LSP servers in CLI |
| lsp_diagnostics | LSP diagnostics | No LSP servers in CLI |

junaidtitan · 4 months ago

Prompt cache invalidation on resume happens because the session JSONL has mutated between sessions — Claude Code re-reads the full file and the cache prefix no longer matches.

Cozempic v1.4.1 reduces cache churn by keeping sessions lean. The guard daemon prunes bloat continuously so the session file changes less between turns. The SessionStart hook also runs a cleanup pass before the first API call.

pip install cozempic && cozempic init

Run cozempic current --diagnose to check your cache hit rate — it now shows read vs creation tokens so you can see how efficiently caching is working.

simpolism · 4 months ago

@blois why was this ticket closed? Still seeing the same poor caching behavior with --resume/--continue on 2.1.92.

github-actions[bot] · 4 months 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.