Skip to content
Control Plane Labs

413 Content Too Large

Client error response defined by RFC 9110 §15.5.14.

Last updated July 27, 2026

Common causes at a glance

  • nginx client_max_body_size (default 1m) rejecting the upload at the edge
  • An API gateway or serverless platform hard cap on request payload size
  • A framework body-parser limit such as Express json({ limit }) or a Django setting
  • Base64 encoding inflating a binary attachment by roughly a third past the limit

Reproduce it

curl -i -X POST --data-binary @big.bin https://api.example.com/v1/upload

What 413 Content Too Large means

The most useful thing to know about 413 is that your application probably never saw the request. Body size limits live in the edge: nginx, an ALB, an API gateway, a WAF, or the framework’s own body parser. Whichever is strictest rejects the upload first, which is why the response often carries generic HTML rather than your API’s JSON error shape.

That layering is also why the fix is rarely in application code. Raising a limit in the framework does nothing if the reverse proxy in front of it caps requests at 1 MB. Find every hop with an opinion about body size and raise all of them, or stop sending large bodies through the request path at all.

What the spec says

RFC 9110 §15.5.14 allows the server to terminate the request if the protocol version permits, or otherwise close the connection — which is why a large upload sometimes dies as a broken pipe instead of a clean status. If the limit is temporary, the spec says the server should include Retry-After; almost nothing does, because size caps are rarely temporary. Note the naming history: the number is 413 in every version, but the registered phrase changed with RFC 9110, so a log line reading “413 Payload Too Large” is an older stack, not a different condition.

What actually causes it

nginx is the usual culprit and its default is low: client_max_body_size is 1 MB unless you changed it. Managed platforms are stricter and often not configurable, so a request that works against a local dev server fails the moment it goes through the gateway. The sneaky variant is encoding overhead: a 9 MB file base64-encoded into a JSON field becomes about 12 MB on the wire and crosses a 10 MB cap the raw file would clear.

How to debug it

Find out which hop rejected you by reading the response body and server header, not just the status. An nginx-generated 413 has a distinctive <center>nginx</center> page; a gateway’s looks like the platform’s own error JSON.

head -c 5000000 /dev/urandom > 5mb.bin
curl -i -X POST --data-binary @5mb.bin https://api.example.com/v1/upload

Bisect by size at 1 MB, 5 MB, and 20 MB; the boundary usually lands on a recognisable default. Then repeat directly against the origin to confirm which layer owns the limit. For anything genuinely large, raising caps is the wrong direction: issue a presigned upload URL so the bytes go straight to object storage and never traverse the proxy chain. Use curl-build to reproduce the exact multipart request your client sends, since browser form encoding and a hand-written command rarely produce the same byte count.

Working examples

curl

# Reproduce with a known-size body and read the error page, not just the code.
head -c 5000000 /dev/urandom > 5mb.bin
curl -i -X POST \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @5mb.bin \
  https://api.example.com/v1/upload

# Multipart behaves differently from a raw body; test the shape you actually send.
curl -i -F 'file=@5mb.bin' https://api.example.com/v1/upload

Python (requests)

import os
import requests

path = "5mb.bin"
size = os.path.getsize(path)

with open(path, "rb") as fh:
    resp = requests.post(
        "https://api.example.com/v1/upload",
        files={"file": (path, fh, "application/octet-stream")},
        timeout=120,
    )

print(size, resp.status_code)
if resp.status_code == 413:
    # The proxy answered, so this is usually HTML rather than your API's JSON.
    print(resp.headers.get("Server"), resp.text[:200])

Node (fetch)

import { readFile } from "node:fs/promises";

const bytes = await readFile("5mb.bin");
const form = new FormData();
form.append("file", new Blob([bytes]), "5mb.bin");

const res = await fetch("https://api.example.com/v1/upload", {
  method: "POST",
  body: form,
});

console.log(bytes.byteLength, res.status, res.headers.get("server"));
if (res.status === 413) console.log((await res.text()).slice(0, 200));

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 4xx codes