[BUG] "an image in the conversation could not be processed and was removed" is NOT removed from the transcript - poisons the prompt cache, every later turn billed as cache-creation (~12x), drains a session in ~3 turns [root cause + fix identified]
Preflight Checklist
- [x] Reproducible on stock Claude Code (no hooks/modifications touch the transcript; images enter only via the normal
Readtool). - [x] Verified against the session
.jsonltoken-usage records, not just the on-screen HP/usage bar. - [x] Searched existing issues. Related to the caching-drain family in #41930 but is a distinct, uncatalogued root cause (image failure, not string-replacement or resume-flag).
What's Wrong?
When the API rejects an image already present in a conversation, Claude Code prints:
API Error: an image in the conversation could not be processed and was removed. Re-read the file with a different approach if you still need it.
The image is removed from that one failed request only. It is left in the persisted session
transcript (.jsonl). So every following turn replays the same poisoned image, re-fails, and —
because the cached prompt prefix no longer matches — the entire history is re-billed as
cache-creation (1.25x) instead of cache-read (0.1x). That is ~12x cost per turn, and it
recurs every turn, emptying a half-full session in ~3 turns. The only escape is abandoning the
conversation; there is no in-product remedy, and the error message ("was removed") actively misleads
the user into thinking it is safe to continue.
This is a third caching-prefix-invalidation bug of the same class as the two in #41930 (which cites
"10-20x cost" for cache-prefix breaks); this one is triggered by an unprocessable image.
Root Cause Analysis (proven with the transcript's own usage accounting)
Each assistant record's message.usage carries cache_read_input_tokens (billed 0.1x) andcache_creation_input_tokens (billed 1.25x). Numbers below are copied verbatim from one dead
session (627f94c4-…jsonl, model claude-opus-4-8); line numbers are .jsonl indices.
Healthy steady state - large prefix is CACHE-READ, only a few thousand new tokens/turn:
| line | cache_read | cache_creation |
|-----:|-----------:|---------------:|
| L2940 | 769,755 | 762 |
| L2967 | 776,472 | 1,678 |
| L3011 | 787,296 | 1,401 |
| L3049 | 795,596 | 2,942 |
Image error fires at L3067. Immediately after, the prefix flips from READ to CREATE and the
error recurs every turn:
| line | cache_read | cache_creation | note |
|-----:|-----------:|---------------:|------|
| L3068 | 23,760 | 763,100 | whole prefix re-written as new |
| L3077 | 26,190 | 759,539 | again, next turn |
| L3088 | - | - | …could not be processed again |
- Healthy turn: ~790,000 tokens as cache-READ -> ~79,000 effective billed units (x0.1).
- Poisoned turn: ~763,000 tokens as cache-CREATE -> ~954,000 effective billed units (x1.25).
- ~12x per turn, recurring, until the session is spent.
Field observation (user-side, independent of the token math above): we run internal
session-usage tracking. On a freshly started account at ~100% of session budget, once a
poisoned image was in the conversation, three replies consumed ~85% of the budget (down to
~15% remaining) — exactly what the 12x cache-creation figure predicts. This is trivially
reproducible; it is not an edge case.
Trigger: the conversation held several large inline images (e.g. a user turn with a 629,660-char
base64 PNG, ~470 KB decoded; another at 425,936 chars). One became un-processable server-side; the
failure was never made durable in the transcript, so it replayed indefinitely. (Note: Claude Code
stores each image twice in the .jsonl - once under message.content[].source and again undertoolUseResult.file.base64.)
Steps to Reproduce
- Build up a long session with a large cached prefix and
Readseveral phone-screenshot-sized PNGs. - Trigger/observe the "an image in the conversation could not be processed and was removed" error.
- Send a few more normal turns.
- Inspect the
.jsonlusageobjects:cache_read_input_tokenscollapses,cache_creation_input_tokensspikes to the full prefix on every turn, and the error recurs. Session budget craters in ~3 turns.
What Should Happen?
On an unprocessable-image error, Claude Code should evict the image from the persisted transcript
(replace with a stub like [image removed: unprocessable]), not just from the one failing request,
so subsequent turns neither replay it nor re-break the cache. A recoverable image error must not
force a full-prefix cache-creation every following turn. Bonus: fix the misleading "was removed"
message and/or offer a one-click "remove bad image and continue."
Error Messages / Logs
API Error: an image in the conversation could not be processed and was removed. Re-read the file with a different approach if you still need it. (recurs every turn after first occurrence; see usage tables above.)
Environment
- Claude Code, VS Code native extension
- Model:
claude-opus-4-8 - OS: Windows 10
- Stock install; no transcript-modifying hooks.
For Anthropic — pull the raw traffic directly: account trenttompkins@gmail.com, affected
session id 627f94c4-85bf-411c-b981-a7148e790da4. The usage records quoted above (the
cache_read -> cache_creation flip at the first image error) are in that session's request log;
you can verify every number server-side.
Community-Discovered Workaround
The instant the error appears, /clear or start a new conversation before sending any further
turn. Do not send another message in the poisoned session. (A local script can strip both base64
copies out of the .jsonl so the image stops replaying, but end users should not have to.)
Related Issues
- #41930 (usage-drain umbrella) - this is a third, distinct cache-prefix-invalidation root cause in that family, triggered by image failure rather than string-replacement or the resume/continue flag.