I was reading the platform release notes from the top on a Friday afternoon when one line from 10/28 stopped me. The maximum request size for the Messages API and the Token Counting API had gone from 32MB to 128MB.
That sent me back to a small script I wrote a while ago, the one that bundles images and hands them to Claude. Its preflight check opens with 32 * 1024 * 1024, hard-coded. My first thought was that I could just change it to 128 and move on.
Then I opened the API overview, and it wasn't that simple. The increase applies to the first-party API only. Bedrock is listed at 20MB, Claude Models on Google Cloud at 30MB or 32MB depending on who manages it, and each route has its own number.
So the first thing I'd like to say is this: before you change the constant to 128, check which route your requests actually travel. What follows is how I checked, and the checker I ended up with.
What changed, and what didn't
This table is what I could confirm from the official pages, as of 2026-10-30.
| Route / endpoint | Max request size | Changed on 10/28? |
|---|---|---|
| First-party Messages / Token Counting | 128MB | Raised from 32MB |
| First-party Message Batches | 256MB | Not mentioned in the release note |
| First-party Files API | 500MB | Not mentioned in the release note |
| First-party Sessions / Agents / Environments | 32MB | Not mentioned in the release note |
| Bedrock | 20MB | Not covered |
| Claude Models on Google Cloud (Google managed) | 30MB | Not covered |
| Claude Platform on AWS / Google Cloud (Anthropic managed) / Microsoft Foundry | 32MB (Messages and Token Counting) | Not covered |
The release note says "raised" only for first-party Messages and Token Counting. Batches and the Files API don't appear in that entry. The overview shows their current numbers, but I couldn't tell whether those values are new or were always there.
Going over the limit returns a 413 request_too_large. On the first-party API the docs say Cloudflare returns it before the request reaches the API servers, so the model never reads a character.
Three things to check before touching the number
Before rewriting any constant, I now look at these in order.
- Which route does my request take? A direct
Anthropic()client, a Bedrock client, or a gateway in between? If there's a gateway, its own limit applies too. - Which endpoint am I sending to? Messages, Batches, or a Files API upload that I reference later. The limits differ.
- Am I sending images or PDFs inline as base64? Base64 inflates the data to roughly 4/3 of its original size. What counts against the limit is the whole inflated request, not the size of the files on disk.
The third one is where I misread numbers most easily, so the next sections use real figures.
A checker with per-route limits
The point is to answer one question before sending: can this bundle go through this route as it is? It returns OK, WARN or NG. All the limit numbers live in a single dictionary and nowhere else.
"""Preflight checker: which limit applies to my route?"""
import base64
import json
import math
import os
import sys
MB = 1024 * 1024
# Messages limit per route, in MB, from the API overview.
# These can change, so the numbers live only in this dict.
LIMITS_MB = {
"first-party": 128,
"bedrock": 20,
"google-cloud-google-managed": 30,
"platform-on-aws": 32,
"google-cloud-anthropic-managed": 32,
"foundry": 32,
}
def inline_size(path: str) -> int:
"""Bytes an image/PDF takes when sent inline as base64 (JSON wrapper excluded)."""
raw = os.path.getsize(path)
return 4 * math.ceil(raw / 3)
def request_bytes(payload: dict) -> int:
return len(json.dumps(payload, ensure_ascii=False).encode("utf-8"))
def verdict(total: int, route: str, margin: float = 0.9) -> str:
limit = LIMITS_MB[route] * MB
if total > limit:
return "NG"
return "WARN" if total > limit * margin else "OK"
if __name__ == "__main__":
route = sys.argv[1]
files = sys.argv[2:]
content = []
for f in files:
with open(f, "rb") as fh:
data = base64.standard_b64encode(fh.read()).decode()
content.append({"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": data}})
content.append({"type": "text", "text": "Please classify these."})
payload = {"model": "claude-sonnet-5", "max_tokens": 1024, "messages": [{"role": "user", "content": content}]}
total = request_bytes(payload)
print(f"route={route} files={len(files)} total={total / MB:.2f}MB limit={LIMITS_MB[route]}MB -> {verdict(total, route)}")A few reasons it's written this way:
- It judges the assembled payload, not the files on disk. That way the base64 inflation, the text and the JSON wrapper are all counted.
- WARN starts at 90% of the limit. I wouldn't trust a request that lands right at the line, since headers or a gateway might add a few bytes.
- The route is a required argument. If it defaulted to the first-party API, a bundle meant for Bedrock would slip through silently.
What it printed
In a sandbox I made six files of 3,500,000 random bytes each and bundled them as images. They aren't real images, but for a size check that doesn't matter. Nothing was sent to the actual API.
raw per file: 3500000 bytes
route=first-party files=5 total=22.25MB limit=128MB -> OK
route=bedrock files=5 total=22.25MB limit=20MB -> NG
route=foundry files=5 total=22.25MB limit=32MB -> OK
route=bedrock files=4 total=17.80MB limit=20MB -> OK
route=first-party files=6 total=26.70MB limit=128MB -> OK
The same five files are OK on one route and NG on another. Each file is about 3.3MB on disk, yet it takes about 4.45MB once encoded. Five of them come to 22.25MB, which is past Bedrock's 20MB.
Counting 3.5MB-equivalent images only, this is how many fit in one request:
| Route | Limit | Approx. images per request |
|---|---|---|
| First-party API | 128MB | 28 |
| Google Cloud (Google managed) | 30MB | 6 |
| AWS / Foundry and similar | 32MB | 7 |
| Bedrock | 20MB | 4 |
What I noticed here is that I used to remember the rule as "stay under 32MB and you're safe." I never thought it meant a wall between the fourth and fifth image on Bedrock. You can't see where the wall is by editing one number.
Pitfalls after the limit goes up
I don't think a higher limit makes it good design to send big bundles as they are. This is closer to a rule of thumb than a measurement: the bigger a request is, the more you resend when it fails.
- A retry sends the same bytes again. If a 100MB request is cut off midway, every retry pushes 100MB again.
- If you reference the same images repeatedly, uploading once to the Files API feels like the better fit. It accepts up to 500MB. I haven't tested it for this article, so check the official docs before relying on it.
- Per-image size and count limits may exist separately from the whole-request limit. That's outside what I checked. If you handle many images, Claude Vision API in Production — Implementation Patterns for Image Analysis, PDF Processing, and OCR is a good companion.
In my own case, the wallpaper app's category classification sends images a few at a time. I wrote about that in Automating Wallpaper Classification with Claude Vision API — Accuracy, Cost, and a Batch Job That Survives Interruptions. Back then I sized the batches by image count; now I think sizing them by bytes is safer.
Where this fits in a preflight
Size is one layer of a larger preflight. The idea of removing 400s and 422s before you call is in A Five-Layer Preflight Design for Claude API — How I Cut Hundreds of 400/422/529 Errors to Zero. If you add a size layer, the verdict function above drops in as it is.
Listing your routes takes less than ten minutes. Keep limit numbers in exactly one place, because they will change. I intend to keep to that on the next day a number moves.
A good first step is to search your scripts for hard-coded values like 32 * 1024 * 1024 and add a one-line comment saying which route that number was about.