I run a small summarization pipeline on the side, as an indie developer. The input is always a PDF.
For a few weeks, the budget I set at the start of the month and the input tokens I saw at the end of it refused to line up. I had been counting characters — extract the text, multiply by the rate, call it an estimate. The actual usage kept landing several times higher.
My first suspect was my own prompt. It wasn't the prompt. A PDF doesn't arrive as text alone.
Every page arrives twice: as text and as a picture
The PDF support documentation describes what happens to a PDF in three steps. Each page is converted into an image, the text extracted from that page is supplied alongside the image, and Claude reads both before answering.
So the input has two halves. The docs spell out the cost of each: the text side runs roughly 1,500–3,000 tokens per page, and the image side is billed with exactly the same calculation as any other image.
My character-count estimate was missing that second half entirely. Not a rounding error — a whole second stream.
The image half has a per-page ceiling
Claude reads images in 28×28 pixel patches rather than pixels. One image costs ⌈width / 28⌉ × ⌈height / 28⌉ visual tokens.
Two limits sit on top of that: a maximum edge length and a maximum visual token count. Exceed either one and the image is scaled down, aspect ratio preserved, to the largest size that satisfies both. The numbers depend on the model's resolution tier.
| Tier | Models | Max edge | Max visual tokens |
|---|---|---|---|
| Standard | Before Claude 4.7 | 1568 px | 1568 |
| High-resolution | Claude 4.7 and later | 2576 px | 4784 |
With a plain image you can downscale it yourself before sending. With a PDF you can't. The coordinates guide states plainly that PDF pages are rasterized server-side at dimensions you don't control. It appears there as the reason returned coordinates can't be mapped back onto the page, but it lands on cost just as hard.
What you can know in advance is how far a single page can go. I implemented the documented resize rule and counted an A4 portrait page at several rasterization densities.
| Density | Pixels | Standard tier | High-resolution tier |
|---|---|---|---|
| 72 dpi | 595×842 | 682 | 682 |
| 96 dpi | 794×1123 | 1,189 | 1,189 |
| 150 dpi | 1240×1754 | 1,551 | 2,835 |
| 200 dpi | 1654×2339 | 1,551 | 4,756 |
| 300 dpi | 2480×3508 | 1,551 | 4,756 |
Past 200 dpi the number stops moving. One A4 page tops out at 1,551 visual tokens on the standard tier and 4,756 on the high-resolution tier — about 3.07 times more for the same page, purely because of the model generation.
That was a relief, in a way. I never needed to learn the server's rasterization density. Don't chase the number you can't see; bracket with the ceiling that doesn't move. I swapped the whole shape of the estimate around that.
Counting a nine-page A4 document two ways
I used a nine-page A4 portrait document in Japanese, 8,021 characters excluding whitespace. That's 891 characters per page — a thin document, as documents go.
There's a second increase on the text side. The token counting documentation notes that Claude 4.7 and later models use a newer tokenizer, and the same input text produces roughly 30 percent more tokens than it did on earlier models.
| Method | Input tokens | One call on Opus 5 ($5 per million) |
|---|---|---|
| Character count only | ~8,021 | ~$0.040 |
| Text (+30%) + standard-tier page ceilings | ~24,386 | ~$0.122 |
| Text (+30%) + high-resolution page ceilings | ~53,231 | ~$0.266 |
On a current model, that's 6.6 times the character-count estimate. Send that document twenty times a day for thirty days and a $24 month becomes a $160 month. That was the whole gap.
Look at the breakdown and almost all of it is the 42,804 tokens on the image side. The 30 percent on text turned out to be a garnish, in money terms.
A forty-line bracket you can run before sending
This implements the documented resize rule and derives the ceiling from page geometry alone. It never calls the API, so you can run it across a whole folder in an instant.
import math
from pypdf import PdfReader
PATCH = 28 # edge of the patch Claude reads images in
TIERS = {"standard": (1568, 1568), # (max edge, max visual tokens)
"high": (2576, 4784)} # high = Claude 4.7 and later
def visual_tokens(w, h):
return math.ceil(w / PATCH) * math.ceil(h / PATCH)
def fits(w, h, max_edge, max_tokens):
# Edges are padded up to a whole patch, so round up before checking the edge
padded_edge = max(math.ceil(w / PATCH), math.ceil(h / PATCH)) * PATCH
return padded_edge <= max_edge and visual_tokens(w, h) <= max_tokens
def page_ceiling(pt_w, pt_h, tier="high", dpi=300):
"""Visual tokens one page cannot exceed, however finely it is rasterized."""
max_edge, max_tokens = TIERS[tier]
w, h = round(pt_w * dpi / 72), round(pt_h * dpi / 72)
if fits(w, h, max_edge, max_tokens):
return visual_tokens(w, h)
# Binary search the largest aspect-preserving scale that satisfies both limits
lo, hi = 0.0, 1.0
for _ in range(40):
mid = (lo + hi) / 2
if fits(max(round(w * mid), 1), max(round(h * mid), 1), max_edge, max_tokens):
lo = mid
else:
hi = mid
return visual_tokens(max(round(w * lo), 1), max(round(h * lo), 1))
def estimate_pdf(path, tier="high", text_per_page=(1500, 3000)):
pages = PdfReader(path).pages
image = sum(page_ceiling(float(p.mediabox.width),
float(p.mediabox.height), tier) for p in pages)
n = len(pages)
return {"pages": n, "image_tokens": image,
"total_min": n * text_per_page[0] + image,
"total_max": n * text_per_page[1] + image}
print(estimate_pdf("sample.pdf", tier="high"))
# {'pages': 9, 'image_tokens': 42804, 'total_min': 56304, 'total_max': 69804}The dpi=300 default isn't a guess at the server's density. It's a stepping stone: high enough that the answer has already stopped changing.
One thing I checked before trusting it. Feeding 1920×1080, 2000×1500 and 3840×2160 through the function returns 1560, 1564 and 1560 on the standard tier and 2691, 3888 and 4784 on the high-resolution tier — the exact values printed in the vision guide's table. When you transcribe a rule by hand, it's worth reproducing the published numbers before you budget against it.
Note that the measured 53,231 for the nine-page document sits just under the function's 56,304 floor. The documented 1,500–3,000 per page assumes dense pages, so it runs a little high on thin ones. The next step tightens the bracket.
What bites when you verify with count_tokens
The token counting endpoint does accept PDFs, and it's free. It has its own rate limit, separate from Messages — 5,000 requests per minute even on the Start tier.
This is where I lost an afternoon. The endpoint rejects document blocks with a url or file source. Only base64 is counted. The larger the PDF, the more you want it sitting in the Files API behind a file_id — and that's exactly the shape you have to undo, locally, just to count it.
The docs add one migration note worth heeding: don't reuse counts measured on an older model, and recount against the model ID you actually plan to call. That 30 percent surfaces right here.
What I settled on was a combination of the two:
- Bracket the whole folder with
page_ceilingand set the month's budget from the ceiling - Base64 one representative document, call
count_tokensonce, and derive a real per-page figure for that document type - Run on that figure afterwards, redoing step 2 only when the document layout changes
The ceiling doesn't move, so step 1 keeps. Step 2 is free and monthly is often enough.
What I changed: only pages that need to be looked at go as PDF
After seeing the breakdown, I split how the input is handed over.
Pages where the table, the chart or the layout itself carries the meaning still go as PDF. The image-side cost buys something there. Pages where reading the prose is enough get extracted locally and sent as text. Plain text can be uploaded to the Files API as text/plain and referenced from a document block, so the shape of the pipeline barely changed.
The nine-page document was the second kind. All 42,804 image tokens simply disappear.
Don't pay picture prices for pages nobody needs to look at. That single line is what counting the ceilings taught me.
Run one of your own PDFs through page_ceiling and look at the per-page number. Whether it comes back 1,551 or 4,756 decides the order of magnitude of your budget. For the separate ceiling that applies when you batch many images into one request, I wrote up Sending images twenty at a time; for building the PDF analysis itself, there's Claude Vision API implementation patterns.
I may still be missing something in the text-side band, but the ceiling has held for every document I've fed it so far.