●2.1.312 — Fixes Claude overwriting very large CLAUDE.md files and skills going missing after a Bash cd●API — The Messages API maximum request size grew from 32 MB to 128 MB (10/28)●SONNET 4.5 — Retires on the Claude API on 11/30, 31 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…●2.1.312 — Fixes Claude overwriting very large CLAUDE.md files and skills going missing after a Bash cd●API — The Messages API maximum request size grew from 32 MB to 128 MB (10/28)●SONNET 4.5 — Retires on the Claude API on 11/30, 31 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…
The Limit Rose to 128MB, but 176 Images at 17.5MB Still Stop: Count the Wall That Hits Before Bytes
The day the Messages API request limit became 128MB, I checked 176 images on my own disk. Bytes had plenty of room; the image-count cap hit first. Here is a pre-send gate and a chunking function.
I read in the release notes that the Messages API request limit had gone from 32MB to 128MB, and my first thought was that I could now send dozens of images in one go.
What I did next was not call the API. I counted the images on my own disk. As an indie developer I still have 176 images left over from older articles. They add up to 13.11MB, and 17.50MB once base64-encoded, which is under 14% of 128MB.
They still would not go out as one request. The count stopped them, not the bytes. This article walks through that order and the function I now run before sending. All numbers here are pre-send estimates; I did not send these requests, so I am not quoting any server error text.
Four limits, in the order they bite
I pulled the image-related limits from the official vision and API overview pages (checked on 2026-10-30).
What to check
Limit
Source
Images per request
100 (models with a 200k context window) / 600 (all other models)
vision
Above 20 images
Every image must be 3,000px or less on the long edge
vision
One image (base64)
10MB (5MB on Bedrock)
vision
Whole request
128MB (Bedrock 20MB, Google-managed Claude Models on Google Cloud 30MB)
API overview
128MB is the bottom row. For a bundle of images, the top three rows are the ones more likely to bite first. The vision page itself warns that request size can be reached before the 600-image count. On my disk, the order was the other way around.
Counting my 176 images
First I counted bytes only. The files total 13.11MB, and base64 adds about 1.334x (three bytes become four characters). I checked this on a real file: a 348,714-byte JPEG became 464,952 characters, exactly what the formula predicts.
The count was the problem. The longest edge is 1,514px, so the 3,000px rule never applies, and each file is small. But with a 200k-context model, one request holds at most 100 images.
I checked the other side too. Against Bedrock's 20MB, 17.50MB is 87% of the limit. My gate treats 90% (18MB) as the safe zone, so it passes, narrowly. That is not comfortable room.
✦
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 can now tell, with numbers, which limit your own batch of images hits first, instead of assuming a higher limit means you are safe
✦You can now catch the long-edge rule that switches on above 20 images before a production request is rejected
✦You will be able to reuse the same check on other routes (direct API, Bedrock and so on) by changing constants, not logic
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.
This is the script that produced the output above. It reads image dimensions only, with no decoding, so 176 files finish instantly. It needs Pillow (pip install pillow).
#!/usr/bin/env python3"""image_gate.py: check an image set against four limits before sending. 1) count 2) base64 size per image 3) long edge <= 3,000px above 20 images 4) whole request (per route)Usage: python3 image_gate.py <dir> [--route first-party|bedrock|gcp-google|aws-foundry]Exit codes: 0=ok / 1=over a limit / 2=no images"""import sys, math, pathlibfrom PIL import ImageMB = 1024 * 1024ROUTES = {"first-party": 128, "bedrock": 20, "gcp-google": 30, "aws-foundry": 32}PER_IMAGE_B64 = {"first-party": 10, "bedrock": 5, "gcp-google": 10, "aws-foundry": 10} # aws-foundry per-image limit unverified: placeholderMAX_IMAGES = 100 # models with a 200k context window; other models allow 600MANY_IMAGES, MANY_EDGE = 20, 3000B64 = lambda n: 4 * math.ceil(n / 3)EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif"}def main(): root = pathlib.Path(sys.argv[1]) route = sys.argv[sys.argv.index("--route") + 1] if "--route" in sys.argv else "first-party" files = sorted(p for p in root.rglob("*") if p.suffix.lower() in EXTS and p.is_file()) if not files: print("no images"); sys.exit(2) total = 0; over_one = []; over_edge = []; edges = [] for p in files: b = B64(p.stat().st_size); total += b + 87 # 87 = JSON envelope of one content block if b > PER_IMAGE_B64[route] * MB: over_one.append(p.name) with Image.open(p) as im: w, h = im.size # reads the header only edges.append(max(w, h)) if max(w, h) > MANY_EDGE: over_edge.append((p.name, max(w, h))) bad = 0 print(f"route={route} files={len(files)} wire={total/MB:.2f}MB / {ROUTES[route]}MB max_edge={max(edges)}px >3000px={len(over_edge)}") if len(files) > MAX_IMAGES: print(f"NG count {len(files)} > {MAX_IMAGES}"); bad += 1 if over_one: print(f"NG single image over limit: {over_one[:3]}"); bad += 1 if len(files) > MANY_IMAGES and over_edge: print(f"NG above 20 images, {len(over_edge)} over 3,000px (e.g. {over_edge[:2]})"); bad += 1 if total > ROUTES[route] * MB * 0.9: print("NG request above 90% of the route limit"); bad += 1 print("OK" if not bad else f"NG {bad} issue(s)") sys.exit(1 if bad else 0)main()
Three design choices are worth explaining.
Per-route constants live in dictionaries. Swapping 128MB for Bedrock's 20MB reuses the same check on another route. I wrote about holding constants per route in the piece on Bedrock staying at 20MB.
The 3,000px check only applies above 20 images. With 19 images, a 6,000px image passes. A rule that changes with the count is easy to miss when you only skim the table.
Three exit codes keep "no images found" separate from "over the limit", so a nightly job or CI step cannot confuse them.
When it fails, chunk in order
On NG, I split rather than give up. This function bundles images in order while watching both count and bytes.
import math, pathlibMB = 1024 * 1024B64 = lambda n: 4 * math.ceil(n / 3)def chunk_images(paths, max_count=100, max_wire_mb=115.0, overhead=87): """Bundle in order so that both count and bytes stay within limits.""" chunks, cur, cur_bytes = [], [], 0 for p in paths: w = B64(pathlib.Path(p).stat().st_size) + overhead if cur and (len(cur) >= max_count or cur_bytes + w > max_wire_mb * MB): chunks.append(cur); cur, cur_bytes = [], 0 cur.append(p); cur_bytes += w if cur: chunks.append(cur) return chunks
Running it on the same 176 images:
{} -> 2 chunks [100, 76]{'max_count': 20} -> 9 chunks [20, 20, 20, 20, 20, 20, 20, 20, 16]{'max_wire_mb': 16.2} -> 2 chunks [100, 76] # 90% of 18MB as the byte cap
Look at lines two and three. Lowering the byte cap for Bedrock's 20MB still gave two chunks. The images are small, so the 100-image count is what binds. With several-MB photos the bytes would bind first. Which wall comes first depends on the bundle.
Putting it into a routine
Here is the order I wired it into my own image-sending step. My recommendation is to put the gate in front of the send, and only go to chunking on NG.
Gather the images into one folder (or list) and run image_gate.py.
On exit code 0, send them as one request.
On exit code 1, read the NG line. For count, pick max_count; for bytes, pick max_wire_mb, then bundle with chunk_images.
If a bundle above 20 images has anything over 3,000px, shrink the long edge before bundling (keep re-encoding as a separate step).
When you change routes, change only --route and repeat the same steps.
The NG line always says which limit was exceeded, because each one needs a different fix: count means split, long edge means resize, and per-image bytes means compress.
Pitfalls
The image cap depends on the model. 100 for a 200k context window, 600 otherwise. When you switch models, update MAX_IMAGES too.
The rules change between 20 and 21 images. Adding a 21st image applies the 3,000px limit to every image. You can also chunk at 20 (the second config above).
I found no source for the per-image limit on aws-foundry. It is a placeholder, as the code comment says. Check your own route's documentation before relying on it.
The base64 estimate excludes the rest of the request. If your prompt or other content blocks are large, raise overhead or lower the 90% safe zone.
For huge PDFs or images you reuse, the Files API lets you reference a file instead of embedding it. I compare the options in the Files API article.
What to do next
Run image_gate.py on your own image folder first. I felt reassured by the byte headroom, and the count failure surprised me. When a limit goes up, do not look at the new number; check which number your own bundle hits first. That is what I took from this.
For pre-send size estimation in general, see designing around 413 request too large. Once you decide how to split a bundle, the next time a limit moves you only edit one constant.
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.