PDF page images add ~190KB/page to every subsequent request but are near-invisible in the context meter

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 3 comments · opened Jul 27, 2026

What happened

Reading a multi-page PDF injects one base64 JPEG per page into the conversation (~190 KB of base64 each). These images stay in the message history and are re-sent with every subsequent request, but they are effectively invisible in the context meter because their token cost is small relative to their byte cost.

A session of mine became permanently unusable at 78,859 tokens — roughly 5% of a 1M context window — because a single 8-page PDF read had added ~1.5 MB of base64 to every request.

The numbers

One Read call with pages: "1-8" produced:

| | |
|---|---|
| images emitted | 8 (JPEG, 850x1100 px each) |
| base64 added | 1.49 MB |
| token cost | ~10,000 (~1,250/image) |
| total request payload after | 1.61 MB |
| of which images | 93% |
| context meter showed | 78,859 tokens |
| effective density | 20.4 bytes/token (normal text is ~6) |

The last successful turn was the one immediately preceding the image insertion. Every request after it failed. Nothing else about the session changed.

Why it matters

The context meter is the only signal a user has for "is this session getting too heavy," and it measures the wrong dimension for image content. A user can wedge a session at a small fraction of its context limit with no warning and no indication of the cause. From the user's side it presents as "this conversation randomly stopped working."

Recovery is also awkward: re-running the read to get the content back regenerates the same images and re-breaks the session. The workaround is to extract the PDF text separately and read that instead (~10K tokens of text vs 1.5 MB of images).

Reproduction

  1. Read any multi-page PDF via the Read tool with a pages range.
  2. Compare the serialized request payload size against the reported context usage.

Payload grows ~190 KB per page while the context meter barely moves.

Suggested fixes

Any of:

  • Account for image byte payload in the context/usage display, not just tokens.
  • Warn when a single tool result adds more than some threshold of payload.
  • Default PDF reads to text extraction, with images opt-in.
  • Drop or downsample page images once they fall outside a recency window.

Environment

  • Claude Code 2.1.220
  • macOS (darwin 25.1.0)
  • Model: Opus, 1M context

View original on GitHub ↗

3 Comments

betson-g · 2 months ago

Hit this too, with a variant that I think widens the scope of the bug in three ways: it is not only the per-page image path, the payload is stored twice, and on Bedrock it surfaces as a 400 that names nothing about size.

The variant: whole-document read, no pages range

I read two PDFs with plain Read calls (no pages parameter), 1.6 MB and 379 KB on disk. Instead of per-page JPEGs, each became a single base64 document block:

| | |
|---|---|
| PDFs read inline | 2 |
| base64 added to message history | 2.75 MB |
| message-only request payload after | 4.23 MB |
| of which attachments | 65% |
| transcript file size | 8.48 MB |
| context meter at time of failure | 54% |

So the "~190 KB/page" accounting understates it for full-document reads. There is no page cap in that path at all: the whole file goes in at roughly 1.37x its on-disk size and is re-sent every turn.

It is stored twice in the transcript

Each attachment appears in the session .jsonl in two places: once as the document block in message.content, and again in a top-level toolUseResult field on the preceding row. Two attachments produced four multi-MB rows. That is the same duplication reported for image tool_results in #80175, so it is not specific to screenshots.

The failure signature is misleading

Every request after the ceiling was crossed returned:

API Error: 400 Incoming request does not contain a valid payload.

That is a Bedrock edge rejection (tool IDs are toolu_bdrk_*), returned pre-inference in about 1.5 seconds. It says nothing about size, tokens, or attachments, so there is no way to connect it to a Read call. It also fires on any subsequent message, including asking "why the api error", which makes the session look corrupted rather than oversized.

The delay is the reason it is invisible

7 hours 31 minutes and roughly 100 turns passed between the PDF reads and the first failure. The reads themselves succeeded and the work continued normally. By the time anything broke, the cause was far out of view, and the context meter said 54%. Agreed with your framing: from the user's side this presents as a conversation that randomly stopped working.

Recovery is possible, which points at a fix

Rather than losing the session, I edited the .jsonl: replaced the two document blocks with a one-line text placeholder and blanked the matching toolUseResult fields, leaving every other row byte-identical.

| | before | after |
|---|---|---|
| message payload | 4.23 MB | 1.48 MB |
| transcript file | 8.48 MB | 2.98 MB |

All 944 rows kept, tool_use/tool_result pairing intact (0 unmatched), parentUuid chain intact (0 broken), and --resume then worked with the full conversation history available. The trailing 400 rows are flagged isApiErrorMessage: true and are filtered on resume, so they did not need removing.

Two things follow from that. First, dropping stale attachment blocks is safe and sufficient, which supports your last suggested fix. Second, since hand-editing a transcript restores a session cleanly, a built-in recovery path would help: either a command to strip attachments from the current session, or doing it automatically when a request exceeds the provider ceiling instead of failing. Worth noting that the two feature requests for selective image removal (#52224, #37461) were both closed without shipping, and #74758 and #83206 are the same wedge on the resume path.

Environment

  • Claude Code 2.1.218
  • macOS 26.5.2 (darwin 25.5.0)
  • Model: Opus 4.8 via Bedrock, 1M context
zdayang · 2 months ago

The PDF page images and full-document blocks described here are both patterns Context Offload scans for. It's a free local Skill that creates a clean-session handoff without uploading, printing Base64, or modifying the transcript: Context Offload.

If you try it, candidate count plus Claude Code version is enough; please keep the transcript private. (Disclosure: I maintain it.)

jsboige · 1 month ago

A fresh Windows/VS Code reproduction shows the same failure mode, but with a larger single Read result and a hard context_length_exceeded loop rather than a 20 MB transport error.

Reproduction measured from the session JSONL

  • Claude Code: 2.1.7 (VS Code extension, Windows 11)
  • PDF read: pages: "1-15"
  • Tool result: 15 JPEG image blocks plus 177 chars of text
  • Individual base64 lengths: 203,284–342,952 chars/page
  • Serialized JSONL record: 4,388,885 bytes
  • Multimodal tool results elsewhere in the session: 0 — this one PDF call was the only such record
  • Session transcript size at diagnosis: 54,139,366 bytes

Immediately after that result, the next model request failed with:

API Error: context_length_exceeded
Your input exceeds the context window of this model.

Every later /continue failed identically. /compact also failed to produce a summary because the compaction request itself inherited the oversized context. This repeated overnight until the session was manually continued through a fresh compacted handoff.

Why the existing settings did not protect the session

The live settings declared:

CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=95
CLAUDE_CODE_AUTO_COMPACT_WINDOW=280000
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000

Those are token-window controls, not a cap on a single multimodal tool result. Read exposes a page-count limit (pages, maximum 20), but no byte budget or text-only mode. Asking for 15 pages is therefore schema-valid while still injecting a 4.39 MB inline result. The failure happened before auto-compaction could recover, and the context meter/settings gave no actionable warning.

This supports all four proposed fixes in the issue, especially a pre-injection byte budget for multimodal tool results. A safe behavior would be to stop or split before the result is committed to history, persist the oversized payload out of context, and return a textual pointer/summary. A PDF mode: "text" | "visual" would also prevent this for text-first research reads.

Related: #80449 (text-only PDF mode), #26018 (payload preflight), and #13480 (poisoned image history; reportedly fixed, but /compact was not a viable recovery here).