●2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server●09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days notice●MCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by hand●NEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to it●WINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a cause●HANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one●2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server●09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days notice●MCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by hand●NEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to it●WINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a cause●HANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one
I Send Images Twenty at a Time — The Day the 21st Image Changed the Rules for the Other 61
Sending 62 images in one request failed with invalid_request_error. The cause was the image count, not the payload size. Here is how I recounted visual tokens as 28px patches and built batches that respect count, dimensions, and payload at once.
One morning I tried to hand an entire asset folder over for sorting. I packed all 62 images into a single request, and what came back was not a set of labels. It was invalid_request_error.
My first suspicion was payload size. But when I measured it, the base64 payload came to 15.19 MB — less than half of the 32 MB request ceiling. Not one image was anywhere near the 10 MB per-image limit either.
The request had failed on count, not on bytes.
The 21st Image Changes the Terms for the Other 61
There is a sentence in the "Request limits" section of the docs that is easy to skim past. When a single request contains more than 20 images, a stricter per-image dimension limit applies to every image in that request. Images that exceed it are rejected with an invalid_request_error whose message mentions "many-image requests."
So the 21st image is not simply refused on its own. It raises the bar for images one through twenty as well. There are only two ways around it: keep every image at 2000 px or under on both sides, or keep the request to 20 or fewer image and document blocks. The second is the sturdier of the two, because it doesn't constrain what your source material is allowed to look like.
What counts toward that threshold is broader than I assumed. Images you resend as conversation history count. Images nested inside tool_result — screenshots returned to computer use, for instance — count. On Amazon Bedrock and Google Cloud, document blocks such as PDFs count too. The longer an agent loop runs, the closer you drift to the threshold even on turns where you only added one picture yourself.
Of my 62 images, 8 had a long edge over 2000 px. Split into groups of twenty, all 8 go through untouched. Only when they rode in one combined request did those same 8 become the ones that took the whole thing down.
Visual Tokens Are Tiles, Not a Division of Area
Fixing the estimate meant fixing how I was counting in the first place.
I had been using "area divided by 750." That is fine for getting the order of magnitude right. It is far too coarse for deciding whether you are just under a limit. The actual rule cuts the image into 28×28 pixel tiles and counts them.
The two separate ceilings are the part that matters. Claude then pads every image — resized or not — out to the next multiple of 28 on the bottom and right edges. That padding holds no content, so when you work with coordinates you normalize against the resized dimensions, never the padded ones.
Downscaling is also governed by two conditions rather than one. Each model has an edge limit and a visual token limit, and Claude picks the largest aspect-preserving size that satisfies both.
Tier
Models
Max long edge
Max visual tokens
Standard
All other models
1568 px
1568
High-resolution
Claude 4.7 and later
2576 px
4784
For photos and screenshots, the token limit is usually what binds first. The edge limit only takes over on elongated images — panoramas, tall phone screenshots, that sort of shape.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to count visual tokens from your own asset dimensions first, and correct your estimate before the day a newer model tier triples your image input
✦You will be able to avoid the boundary where adding a 21st image rejects the entire request with invalid_request_error, before a production batch hits it
✦You will be able to tell whether compression or resizing is what actually moves your bill, and stop spending time on the optimization that does neither
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Rather than keep guessing, I ran the real folder: a working mix of icons, OGP cards, and distribution artwork from the projects I maintain as an indie developer.
Original size
Count
Standard tier sends
Tokens
High-res tier sends
Tokens
1200×1200
12
1092×1092
1,521
1200×1200
1,849
1920×1081
8
1456×820
1,560
1920×1081
2,691
2400×2400
4
1092×1092
1,521
1932×1932
4,761
3040×1712
4
1456×820
1,560
2576×1451
4,784
1200×630
4
1200×630
989
1200×630
989
48×48
5
48×48
4
48×48
4
The 62 images total 53,532 visual tokens on the standard tier and 93,888 on the high-resolution tier. On the standard tier, 31 of the 62 were being resized server-side before Claude ever saw them.
Before trusting any of this, I checked my estimator against the conversion table in the docs. A 1920×1080 image becomes 1456×819 at 1,560 tokens; a 3840×2160 image becomes 2576×1449 at 4,784 on the high-resolution tier; an A4 page scanned at 130 DPI, 1075×1520, becomes 924×1307. All three matched. If that step disagrees, every decision downstream is built on sand.
That A4 row was the one that surprised me most. Neither side exceeds 1568 px, yet it still gets resized — 39 × 55 = 2,145 tokens puts it over the token limit rather than the edge limit.
Compressing the Files Did Not Remove a Single Token
The answer to something I had wondered about was sitting in the same table.
The eight 1920×1081 files range from 14 KB to 65 KB — more than a fourfold spread. All eight cost exactly 1,560 visual tokens. The twelve 1200×1200 files scatter between 177 KB and 202 KB, and every one of them costs 1,521.
For a while I had been lowering JPEG quality before upload and believing I had made things cheaper. It didn't work out. The bill never moved, and all I had added was a little more compression noise.
Count changes the shape of the request, dimensions change the cost, and file size changes neither.
That is not an argument against compression. It is an argument about where compression lands. File size affects round-trip latency and the 10 MB per-image and 32 MB per-request ceilings. If you want to affect cost, the thing to touch is pixel dimensions. Keeping those two on separate dials is what finally stopped me from second-guessing my pre-upload step.
Tall Images Never Use the Budget They Were Given
Something else only became visible once I counted.
A phone-shaped 1290×2796 image sent to the standard tier is resized to 723×1568, which costs 1,456 tokens. The ceiling is 1,568, so it stops 112 tokens short. On elongated images the edge limit binds first, and the token budget goes partly unused.
Given the same 1,568-token allowance, a roughly square image carries more pixels into the model. Classification and reading accuracy ultimately track the pixels Claude actually saw, which means tall assets are being judged under worse conditions than wide ones.
The fix was cropping rather than shrinking. For tall source material I now crop out only the region the decision depends on, bringing the shape closer to square before sending. Total pixel count goes down while the resolution Claude sees goes up — a small inversion I still find pleasing.
Moving Up a Model Generation Makes the Same Image Cost More
The high-resolution tier turns on automatically for Claude 4.7 and later models. There is no beta header and no client-side opt-in. That is exactly what makes it easy to miss.
In my own numbers, the four 2400×2400 files went from 1,521 to 4,761 tokens, and the four 3040×1712 files from 1,560 to 4,784. Roughly 3.1×. Across all 62 the multiplier settled at 1.75×, but only because small icons drag the average down; a pipeline that carries only large assets sees the full 3×.
Not one line of my code had changed. There is a path where swapping the model name for a newer generation triples your image input tokens, and I now mention that whenever I explain an estimate to someone else. If the work depends on fine detail, 3× is worth paying. If you are sorting by color and composition, shrinking before you send is the more honest answer.
Making Silent Resizing Loud
Resizing images yourself before upload keeps the image you hold and the image Claude sees identical. That promise only holds while your pipeline keeps producing the right sizes, though. Add a new source of assets, or move to a model on a different tier, and server-side resizing quietly returns.
There is a setting that converts that drift into a visible error. You attach transformations to the image block.
With that in place, an image that would have been resized is rejected with a 400 invalid_request_error, and the message carries both its current dimensions and the largest dimensions that fit. You rescale to the reported target and resend. The default is downsize, which keeps the automatic behavior.
The per-image granularity turned out to be the practical part. A screenshot whose coordinates you intend to act on can carry error, while a logo in the same request carries nothing at all. The Token counting endpoint honors the setting too, so you can verify that an image passes unresized before spending any inference. That check applies to embedded base64 only — images passed by URL or file ID are evaluated at Messages time.
One thing not to over-trust: this setting stops resizing and nothing else. The 8000 px hard limit and the many-image limit from earlier are separate rejections, and no transformations value gets you past them.
Planning Batches Against All Three Limits
Three limits have come up here. Count, where crossing 20 changes the terms for every image. Dimensions, which set visual tokens and therefore cost. Payload, which meets the 32 MB request ceiling. A batch built by looking at only one of them will eventually fail on one of the other two.
The order I check them in is always the same three steps.
Count how many images have a long edge over 2000 px. If even one does, that batch is pinned to 20 images or fewer.
Total the visual tokens for the tier of the model you are calling. Get the tier wrong and every downstream estimate shifts with it.
Compare the base64 payload total against what is left after prompt text and history. Never let images fill the ceiling on their own.
Wanting those three lined up on every run is what turned the check into a script.
"""Plan image batches before sending.Respects count, dimensions, and payload at the same time."""import mathimport osimport sysfrom PIL import ImagePATCH = 28 # one visual token covers a 28x28 patchMANY_IMAGE_THRESHOLD = 20 # above this, every image gets a stricter size limitMANY_IMAGE_MAX_EDGE = 2000 # long edge imposed on each image in a 21+ requestREQUEST_BYTE_BUDGET = 30 * 1024 * 1024 # headroom under the 32MB request ceilingTIERS = { "standard": {"max_edge": 1568, "max_tokens": 1568}, "high": {"max_edge": 2576, "max_tokens": 4784},}def visual_tokens(width: int, height: int) -> int: """One 28x28 patch is one visual token.""" return math.ceil(width / PATCH) * math.ceil(height / PATCH)def _fits(w: int, h: int, max_edge: int, max_tokens: int) -> bool: # Judge on padded edges, since both are rounded up to a multiple of 28. return (math.ceil(w / PATCH) * PATCH <= max_edge and math.ceil(h / PATCH) * PATCH <= max_edge and visual_tokens(w, h) <= max_tokens)def served_size(w: int, h: int, tier: str = "standard") -> tuple: """Dimensions after server-side resizing. Returns the input unchanged if it fits.""" limits = TIERS[tier] max_edge, max_tokens = limits["max_edge"], limits["max_tokens"] if _fits(w, h, max_edge, max_tokens): return (w, h) if h > w: # transpose tall images into the same search rw, rh = served_size(h, w, tier) return (rh, rw) aspect = w / h lo, hi = 1, w # lo always fits; hi never fits while lo + 1 < hi: mid = (lo + hi) // 2 if _fits(mid, max(round(mid / aspect), 1), max_edge, max_tokens): lo = mid else: hi = mid return (lo, max(round(lo / aspect), 1))def inspect(path: str, tier: str = "standard") -> dict: with Image.open(path) as im: w, h = im.size sw, sh = served_size(w, h, tier) raw = os.path.getsize(path) return { "path": path, "size": (w, h), "served": (sw, sh), "tokens": visual_tokens(sw, sh), "resized_by_server": (sw, sh) != (w, h), "payload": math.ceil(raw / 3) * 4, # base64 inflates bytes by about 4/3 "needs_shrink_for_large_batch": max(w, h) > MANY_IMAGE_MAX_EDGE, }def plan(paths: list, tier: str = "standard") -> list: """Cut a batch just before it touches either the count or the payload limit.""" items = [inspect(p, tier) for p in paths] batches, current, used = [], [], 0 for item in items: over_count = len(current) + 1 > MANY_IMAGE_THRESHOLD over_bytes = used + item["payload"] > REQUEST_BYTE_BUDGET if current and (over_count or over_bytes): batches.append(current) current, used = [], 0 current.append(item) used += item["payload"] if current: batches.append(current) return batchesif __name__ == "__main__": root = sys.argv[1] tier = sys.argv[2] if len(sys.argv) > 2 else "standard" paths = sorted( os.path.join(dirpath, name) for dirpath, _, names in os.walk(root) for name in names if name.lower().endswith((".jpg", ".jpeg", ".png", ".webp", ".gif")) ) batches = plan(paths, tier) total = sum(i["tokens"] for b in batches for i in b) shrink = [i for b in batches for i in b if i["needs_shrink_for_large_batch"]] resized = [i for b in batches for i in b if i["resized_by_server"]] print(f"{len(paths)} images / tier {tier}") print(f"{len(batches)} batches (max {MANY_IMAGE_THRESHOLD} per batch)") print(f"visual tokens total {total:,}") print(f"resized server-side: {len(resized)}") print(f"long edge over {MANY_IMAGE_MAX_EDGE}px (rejected in 21+ requests): {len(shrink)}") for n, batch in enumerate(batches, 1): mb = sum(i["payload"] for i in batch) / 1048576 tk = sum(i["tokens"] for i in batch) print(f" batch {n}: {len(batch):>2} images / {tk:>6,} tok / payload {mb:5.2f} MB")
Run against the same 62 images, it prints this.
62 images / tier standard
4 batches (max 20 per batch)
visual tokens total 53,532
resized server-side: 31
long edge over 2000px (rejected in 21+ requests): 8
batch 1: 20 images / 4,507 tok / payload 4.77 MB
batch 2: 20 images / 17,520 tok / payload 2.45 MB
batch 3: 20 images / 28,409 tok / payload 4.85 MB
batch 4: 2 images / 3,096 tok / payload 3.12 MB
Two notes on why it is written this way.
The count limit is a fixed constant rather than a tunable. You could instead branch on "if over 20, shrink to 2000 px," but then the decision to lose pixels becomes a function of batch size, and cost and accuracy start moving together. I would rather decide how to slice batches and how large each image should be as two independent choices.
The byte budget is 30 MB rather than 32 MB. Your prompt text, tool definitions, and prior turns all travel in the same request, so a plan that lets images consume the whole ceiling will fail on the day your history grows. The 2 MB of slack is insurance for that day.
Pick one folder you already have and count only this: how many images have a long edge over 2000 px. If the answer is zero, the count limit is still some distance away. If it is even one, the reason for cutting batches at twenty will land on its own.
A word about scope, too. What I hand to Claude is the sorting and tagging — the operational half of the work. As I wrote in Automating Wallpaper Category Classification with the Claude Vision API, restoring photographs of old paper is still something I do with my own hands. Where that line sits is a decision I keep in a different drawer from the one marked cost.
Thank you for reading this far. I hope it gives you a useful place to start recounting.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.