●OPUS — Claude Opus 4.7 is generally available, improving software engineering, long-running coding, and higher-resolution vision●APIKEY — You can now set an expiration on API keys in the Console, with email reminders before keys valid for 7+ days expire●REFLECT — A monthly recap at Settings > Reflect shows your top topics, most active day, and peak hour (beta)●M365 — The Microsoft 365 connector now supports write tools for email, calendar, and OneDrive/SharePoint files●COWORK — Cowork expands to web and mobile, bringing Chat and Cowork into one shared home across devices●DESIGN — Claude Design, a new Anthropic Labs product, lets you co-create designs, prototypes, slides, and one-pagers●OPUS — Claude Opus 4.7 is generally available, improving software engineering, long-running coding, and higher-resolution vision●APIKEY — You can now set an expiration on API keys in the Console, with email reminders before keys valid for 7+ days expire●REFLECT — A monthly recap at Settings > Reflect shows your top topics, most active day, and peak hour (beta)●M365 — The Microsoft 365 connector now supports write tools for email, calendar, and OneDrive/SharePoint files●COWORK — Cowork expands to web and mobile, bringing Chat and Cowork into one shared home across devices●DESIGN — Claude Design, a new Anthropic Labs product, lets you co-create designs, prototypes, slides, and one-pagers
Designing Around Claude API 413 request too large — Preflight Sizing and Splitting
Pack too much text, images, and tool_result into one request and Claude API rejects it with 413 request too large. Here is a code-backed design for measuring request bytes before you send, telling the two kinds of 413 apart, and splitting requests without breaking them.
I noticed the nightly batch had stalled on a 413 only when I opened the logs one morning. The job — part of the indie wallpaper app I run as a solo developer — hands a set of artworks to Claude Vision for tagging, and it had not advanced a single image since the day before. The error was request_too_large. There should have been plenty of token budget left, so why was it being rejected? Chasing the cause led me to a fact that lives only in the corners of the docs: a Claude API request has a "byte" wall that is separate from "tokens."
This article walks through the design I built to get that 413 out of my operations for good. Three ideas carry it. Measure bytes before you send. Learn to tell where the 413 happened. And decide, ahead of time, the order in which you will split a request.
413 Is Not a Token Story
The longer you work with the Claude API, the more you tend to worry only about whether something fits inside the context window. But request_too_large (HTTP 413) is judged by the physical size of the request body, independent of token count.
On the standard Messages endpoint, the whole request is capped at 32MB. Cross that, and you are rejected before the model reads a single character. You rarely reach that number sending text alone, but the landscape changes the moment you mix in images or tool_result.
The easy thing to miss is base64 inflation. When you send images or files inline, you convert bytes to base64, and that conversion swells the real data by about 1.33x. A 4MB PNG on disk occupies 5.3MB in the request. Stack a few, and you hit 32MB far sooner than intuition suggests.
What you pack
The limit that bites
The gap from intuition
Long text only
Mostly tokens
You rarely reach 32MB
Several inline base64 images
The 32MB byte ceiling
base64 inflates them 1.33x
A large tool_result
The 32MB byte ceiling
It accumulates in the history
Multiple large PDFs
A hidden processing-stage limit
Can fail even below 32MB
Measure the Bytes Before You Send
What helped most was measuring the real bytes myself, right after assembling the request and before sending it. Instead of throwing the payload at the SDK and waiting for a 413 to come back, I branch on the way out.
Here is a Python example. The function serializes the message array and measures the byte size of the JSON that will actually go over the wire.
import json# Request ceiling on the standard Messages endpoint (leave a little headroom)MAX_REQUEST_BYTES = 32 * 1024 * 1024 # 32MBSAFETY_MARGIN = 512 * 1024 # 0.5MB of headroomBUDGET = MAX_REQUEST_BYTES - SAFETY_MARGINdef request_byte_size(payload: dict) -> int: """Serialize the way the SDK does and measure the bytes.""" # ensure_ascii=False keeps multibyte text close to real bytes (counted as UTF-8) body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")) return len(body.encode("utf-8"))def will_fit(payload: dict) -> tuple[bool, int]: size = request_byte_size(payload) return size <= BUDGET, size
Two things matter here. First, separators=(",", ":") strips the whitespace so the estimate mirrors what actually ships. Second, encode to UTF-8 before measuring. Multibyte text takes three bytes per character, so if you count characters instead of bytes your estimate runs optimistic.
When will_fit returns False, stop the send and route into splitting. This is the first gate for not causing a 413 rather than reacting to one after it arrives.
✦
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
✦A preflight function that estimates the real bytes of a request, accounting for the 32MB ceiling and the ~1.33x base64 inflation of images and tool_result
✦How to tell a 413 that fails before HTTP transfer from one that fails during Anthropic's processing stage — and the different fix each one needs
✦A priority table for splitting a request without breaking it: downscale images, offload to the Files API, split documents, or move to the Batch API
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.
For a request with images, you can predict how much room each one will take from its size on disk, before base64 encoding. Converting everything to base64 just to measure it is wasteful, so the estimate is a multiplication.
from pathlib import Pathdef estimated_inline_bytes(image_path: str) -> int: """Approximate bytes an image occupies when sent inline as base64.""" raw = Path(image_path).stat().st_size # base64 is about 4/3; add a little for JSON keys and the media-type declaration return int(raw * 4 / 3) + 256def pack_images_within_budget(image_paths: list[str], text_bytes: int) -> tuple[list[str], list[str]]: """Pack only the images that fit into one request; defer the rest.""" used = text_bytes fit, overflow = [], [] for path in image_paths: cost = estimated_inline_bytes(path) if used + cost <= BUDGET: used += cost fit.append(path) else: overflow.append(path) return fit, overflow
In my wallpaper batch, just inserting pack_images_within_budget made the 413 all but vanish. I used to decide crudely — "about twenty images per call" — but artwork resolutions vary, and a few high-detail images were enough to blow past the ceiling. Bundle by bytes, not by count. That alone quieted the morning logs.
The 413 That Fails at HTTP vs. the One That Fails in Processing
This next part is the operational reality the docs do not cover. A 413 actually has two faces.
One is the 413 we have been discussing, where you exceed 32MB and are rejected before HTTP transfer. Preflight byte estimation avoids it.
The other is trickier. The request itself sits under 32MB, yet when you hand over several large PDFs together, a 413 comes back while Anthropic expands the files and extracts text on their side. From what I have observed, it tends to appear once the combined file total crosses somewhere around 6MB, which looks like a processing-resource ceiling separate from the 32MB HTTP limit.
Telling them apart is simple.
Symptom
Likely cause
Fix
Request exceeds 32MB
Pre-transfer HTTP ceiling
Shrink total bytes: downscale, split
Under 32MB but 413 with multiple PDFs
Hidden processing-stage limit
Send 1-2 files per request
413 on a single huge file
That one file is heavy
Offload to Files API, or pre-compress
Before I understood this distinction, I suspected an SDK bug in the "413 under 32MB" case and burned half a day. In truth I was simply greedily bundling too many PDFs. The same 413, but a different place to trim. Once I grasped that one point, the response stopped being guesswork.
Decide the Splitting Order Ahead of Time
When a 413 hits, deciding "how to trim" on the spot is too slow. Decide the priority in advance and put it in code. The order I use is this.
Downscale images — reduce the long edge within the range that does not hurt recognition. For many tasks a long edge near 1500px was the sweet spot between accuracy and size. Try it first; it is the lowest-loss move.
Offload to the Files API — if you reference the same file repeatedly, stop sending inline base64, upload it, and pass a reference. Note, as above, that even with the Files API a per-request ceiling on how much gets expanded still applies.
Split documents — narrow to one or two PDFs per request and push the rest to follow-up calls. This is what fixes the processing-stage 413.
Move to the Batch API — for large, non-urgent work, shift to the asynchronous batch. Per-item constraints remain, but it is a good moment to rethink how you bundle.
There is a reason for that order. The higher an item sits, the smaller its side effect on quality; the lower it goes, the more you rebuild the processing. Arranging it as try the least costly move first lets your automatic-recovery code follow the same shape.
def send_with_413_recovery(client, build_payload, images, docs, text_bytes): """A loop that sends while dodging 413 in stages.""" # Preflight estimate first fit_images, overflow = pack_images_within_budget(images, text_bytes) payload = build_payload(fit_images, docs) ok, size = will_fit(payload) if not ok: # Over budget. Trim in order: downscale images, then split documents fit_images = downscale_until_fit(fit_images, text_bytes, docs) payload = build_payload(fit_images, docs) try: resp = client.messages.create(**payload) except APIStatusError as e: if e.status_code == 413: # Passed HTTP but 413 in processing -> narrow the docs and retry first_half, second_half = split_docs(docs) resp = client.messages.create(**build_payload(fit_images, first_half)) _enqueue_followup(second_half, overflow) # rest goes to the next batch else: raise return resp, overflow
The bodies of downscale_until_fit and split_docs depend on your task, but the skeleton holds. A two-stage stance — prevent with an estimate, and branch a 413 that still arrives by its cause — keeps a batch alive even when it runs unattended.
Small Things That Helped in Practice
A few finer notes that never make it into the docs.
The tool_result in a conversation history swells quietly. When an agent calls an image-returning tool round after round, past tool_result blocks accumulate in the message array and creep toward 32MB. Replace old image tool_result with a summary, or keep the reference and drop the payload, to stay safe.
I also recommend logging your estimates. I write the estimated bytes of every request into structured logs. Seeing "how close each batch runs to the ceiling" lets me fix the bundling before a 413 appears. Unattended automation without instruments is like running a nightly batch in the dark.
And the numbers themselves can change. The 32MB figure and the processing-stage threshold are observations at the time of writing. Shape the design around "measure before sending and branch by cause" rather than around a specific number, and it will not break when a limit moves. Do not chase the numbers; build the habit of measuring into the system. That, for me, was the surest way back to a quiet morning.
I hope this helps in your own implementation. I am still finding my way in places, but if it spares someone else the same detour around 413, that would make me glad. Thank you for reading.
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.