How-to2026-08-276 min read

When a Large PDF Upload Is Rejected: the 100 MiB Request-Body Limit and the Error That Points You the Wrong Way

One request body may be 100 MiB, which is 104,857,600 bytes, multipart framing included. Over the limit, operation endpoints answer 400 with code bad_request and a detail about a failed multipart field, not 413, so a size problem reads like a parameter problem. With Idempotency-Key there is a second 100 MiB limit on the response buffer; over it you get 500 and nothing is cached.

PDF123 · Updated 2026-08-27

A large PDF that will not upload is usually not a corrupt file. The request body hit the per-request limit: 100 MiB, or 104,857,600 bytes. It is measured across the whole body, multipart boundaries and field headers included, and that exact figure passes while one byte more is rejected.

The error that comes back points elsewhere. Operation endpoints answer 400 with code set to bad_request and a detail saying a multipart field failed to read, with no mention of size anywhere. A client that branches on code files this as a parameter error and goes off to check field names, when the thing to change is the file size.

The same 100 MiB also governs the opposite direction. A request carrying Idempotency-Key reads the response into memory before caching it, against the same figure, and over it the caller receives 500 while the operation has already finished. One number, two opposite failure modes.

                     100 MiB = 104,857,600 bytes (this exact figure passes)
                                   |
                +------------------+------------------+
                |                                     |
         inbound (request body)                outbound (response body)
   whole body, multipart boundaries        only with Idempotency-Key,
   and field headers included; one         a cache miss and an
   operation and a pipeline share it       upstream 2xx
   over: 400 + bad_request                 over: 500, nothing cached
   (detail: a multipart field failed)

Figure note: one limit, and the inbound and outbound sides of it return different status codes with opposite consequences.

The limit counts the whole request body, boundaries included

The 100 MiB caps the body of a single request, not the size of one file and not the size after decompression.

  • A single operation and a multi-step pipeline share the figure. Splitting the work into 10 steps inside one /api/v1/pipeline call does not turn the ceiling into 1 GB; the step count affects runtime only.
  • The boundary figure itself passes: a 104,857,600-byte request body goes through, 104,857,601 bytes does not.
  • The body carries each field's headers and the boundary delimiters as well as the file bytes, so the allowance left for a single file is strictly under 100 MiB. A file of exactly 104,857,600 bytes is rejected.

That last point is where practice slips most easily: curl -F adds the boundaries for you, so comparing a file size against that line can never come out right.

Over the limit you get 400 and bad_request

An over-limit response does not use 413, and it carries no wording about size at all. Reproduce it with a body one byte over:

head -c 104857601 /dev/zero > /tmp/over.bin
curl -s -X POST "$API_BASE/api/v1/misc/compress-pdf" \
  -H "X-API-KEY: $API_KEY" \
  -F "fileInput=@/tmp/over.bin"
HTTP/1.1 400 Bad Request
content-type: application/problem+json

{ "code": "bad_request",
  "detail": "failed to read multipart field: Error parsing `multipart/form-data` request",
  "hint": "Fix request parameters or upload a valid PDF.",
  "status": 400,
  "title": "Bad Request",
  "type": "https://pdf123.xyz/developers/errors#bad_request" }

Three things to read together:

  • The status is 400. A failed body read is classified as bad_request here and still goes through problem+json, so a client written for "over the limit means 413" takes the wrong branch.
  • code is bad_request, a real entry in the error code table. It does not fall through to a catch-all; it shares one code with a mistyped field name or a broken multipart encoding, and nothing in that table concerns size.
  • The hint tells you to upload a valid PDF. The file you uploaded is very likely a valid PDF that happens to be a few hundred bytes too large.

The tell, then, is not the status code but the byte count of the body: on a 400 whose detail contains failed to read multipart field, measure the size next instead of walking back through the form.

413 does occur on this site, just not on operation endpoints. Every entry point that accepts a file upload has been raised to 100 MiB, while endpoints that take no upload still run on the HTTP framework's (Rust's axum) default of 2 MiB, where an over-limit body gets 413 and one line of plain text:

head -c 2097153 /dev/zero > /tmp/big.json
curl -s -w '\n%{http_code}\n' -X POST "$API_BASE/api/v1/auth/login" \
  -H 'Content-Type: application/json' \
  --data-binary @/tmp/big.json
Failed to buffer the request body: length limit exceeded
413

One fact, two status codes and two response bodies depending on the endpoint. Carrying either experience across to the other will mislead you.

The other failure with the same number, on the way back

A request with Idempotency-Key caches its response so that a retry can replay it. Caching means reading the response body into memory first, and that read is capped by the same 100 MiB, though it takes a much narrower set of conditions:

  1. The request carried Idempotency-Key
  2. The cache missed, so this key is new
  3. The upstream operation returned 2xx

Failures are not cached and pass straight through, so only a successful large output reaches this limit.

What happens there is the point worth remembering: you get 500, and nothing is written to the cache. The operation has already run, yet the caller sees a failure; because nothing was cached, the retry runs the whole thing again. The idempotency key exists to eliminate duplicate work and fails exactly when it is needed most, dressing a finished job up as a server fault. The cache lives in process memory and is never written to disk, so a restart clears it; for the semantics see Idempotency-Key: Safe Retries for PDF Jobs.

100 MiB is not a configurable platform setting

The figure cannot be changed. No environment variable raises or lowers it, in either direction; a different number means changing code and rebuilding the image. Look for it as a deployment setting and you will not find one.

It is also more than a quota. The bytes of a request body are read into memory in full before processing, so every concurrent large upload holds a comparable amount beside it. Raising the ceiling means accepting a higher memory peak along with it: the number is also what keeps a single request from dragging the process down.

A default self-hosted deployment has no reverse proxy, and the portal does not check size before submitting, so that rejection comes from the server's own limit. Put nginx in front and you will hit nginx first: client_max_body_size allows only 1 MiB by default and answers 413, a shape close to the one in the comparison above and easy to mistake for the same limit.

What to do when you hit it

Cheapest first:

  1. Measure before you send. Compare the request-body byte count against 104,857,600 before the request goes out, and leave room for the boundaries. That beats reading a status code afterwards.
  2. Compress the file under the limit. A scan's size comes mostly from its image layer, and recompressing routinely removes a visible share of it. Compress a PDF runs in the browser, no script needed.
  3. Split the work across calls. When the content divides, split it and send it in a few requests: less trouble than raising the ceiling, and it does not push up the memory that one request holds.
  4. Only change the limit for a hard single-request requirement. That means changing code and rebuilding, and accepting the memory cost from the previous section.

This limit asks the client to decide in advance

Both failures point to one conclusion: the 100 MiB figure has to be worked out by the client before it sends. Inbound, you are answered with bad_request, a code that says nothing about size; outbound, with 500, which looks like a server fault. Counting the bytes before the request leaves is the only way to judge that does not depend on what an error message says.

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool