413 Request Entity Too Large When You Already Raised the Limit
You raised the body size limit and the upload still fails. There are almost always two limits, and the one you changed is rarely the one rejecting you.
A file upload endpoint rejecting a 12 MB file with a 413. I raised the application's limit to 50 MB, redeployed, and got the same 413 in the same number of milliseconds.
The same number of milliseconds is the part worth noticing. If my application had received 12 MB and then decided it was too large, the request would have taken as long as the upload. It failed almost instantly, which meant the rejection happened before my code was involved.
The short answer
There are at least two body size limits in front of every upload: one at the proxy or edge, and one in your application. The rejection comes from whichever is smaller, and raising the other one changes nothing.
Find out which one answered by reading the response headers rather than the status code. A 413 from nginx looks nothing like a 413 from Express, and the difference identifies the culprit in one request.
Tested on nginx 1.27, Node 22.14, Express 4.21.
Which layer rejected you
curl -i -X POST https://app.example.com/upload \
-F "[email protected]" \
-o /dev/null -w "\ntime: %{time_total}s size_upload: %{size_upload}\n"
Read three things from that:
size_upload. If it is far smaller than the file, the connection was closed mid transfer. Something upstream stopped reading. If it equals the file size, the whole body was received and the rejection is deeper in.
time_total. Near instant means an upstream rejection. Proportional to the file size means your application got the bytes.
The Server header and the body. nginx returns a compact HTML error page and Server: nginx. Express returns JSON or a stack trace. Cloudflare returns its own branded page and a cf-ray header. Each is a fingerprint.
In my case: size_upload was 65536, time was 40 ms, and the body was nginx's default error page. My application had never been asked.
Cause one: the reverse proxy in front of you
Ranked by how often it is the answer.
nginx. client_max_body_size defaults to 1 MB. This is the single most common source of a surprise 413 anywhere on the internet.
server {
client_max_body_size 50m;
# applies at http, server or location level; the innermost wins
client_body_timeout 60s; # large uploads on slow links also need this
}
The trap is the directive being set in three places and the innermost silently overriding what you edited. nginx -T | grep -n client_max_body_size prints the full resolved config, including includes, and shows you every occurrence at once.
Apache. LimitRequestBody, which defaults to unlimited but is very often set to something modest in a distro's default config.
Caddy. request_body { max_size 50MB }. Unlimited by default, which means when it does fail it is because somebody set it.
A managed edge proxy. Most platforms enforce a body limit before traffic reaches your machine, and it is configurable per route rather than in a file you control. On Krova Cloud that is a proxy level setting alongside the CORS, header override and IP allow rules, so the limit lives with the route rather than inside the machine. Same principle, different place to look, and the important thing is knowing that the layer exists at all.
A CDN. Cloudflare's free plan caps uploads at 100 MB and no configuration on your origin changes that. If you need more, the answer is uploading directly to object storage rather than through the proxy.
Cause two: your application framework
If the proxy passed the body through, the next limit is yours.
// Express
app.use(express.json({ limit: '50mb' }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
// multer, for multipart, which has its own separate limit
const upload = multer({ limits: { fileSize: 50 * 1024 * 1024 } })
The frequent mistake is setting express.json and expecting it to govern a multipart upload. It does not. express.json only parses application/json. A file upload is multipart/form-data and is handled by multer or busboy, which has an entirely independent limit. Two limits inside one application, and people raise the wrong one constantly.
Others worth knowing:
| Stack | Setting | Default |
|---|---|---|
| PHP | upload_max_filesize and post_max_size |
2 MB and 8 MB |
| Django | DATA_UPLOAD_MAX_MEMORY_SIZE |
2.5 MB |
| Rails | Rack::Utils.multipart_part_limit |
part count, not size |
| Spring | spring.servlet.multipart.max-file-size |
1 MB |
| FastAPI | no default limit | proxy governs |
PHP is the notorious one because upload_max_filesize and post_max_size must both be raised and the smaller wins, which is the same two limit problem recursing one level deeper.
Cause three: it is not a size limit at all
Two lookalikes worth ruling out before you spend an afternoon on config.
A timeout presenting as a failed upload. A large file over a slow connection can exceed client_body_timeout or proxy_read_timeout. The transfer dies partway with a connection reset rather than a clean 413. If your size_upload stops at an oddly consistent point in time rather than at a consistent number of bytes, you have a timeout.
Buffering to a full disk. nginx buffers request bodies to client_body_temp_path before forwarding. If that filesystem is full, uploads fail in ways that look like size limits. df -h /var/lib/nginx takes two seconds and has surprised me more than once.
Prevention
Set the proxy limit slightly above the application limit. If nginx allows 50 MB and your app allows 45 MB, oversized uploads reach your code and you can return a useful error message with a real limit in it. Inverted, the user gets nginx's default HTML error page and you get nothing in your application logs, which is why this class of bug is so often invisible to the team that owns the endpoint.
Log the rejection where it happens. A 413 that only exists in the proxy's access log will not appear in your monitoring. Alert on it there.
Return the limit in the error. {"error": "file too large", "max_bytes": 47185920} costs nothing and eliminates a support ticket.
Enumerate the layers once and write them down. CDN, edge proxy, reverse proxy, application server, framework parser. Five possible limits on a single request is normal, and a diagram in the runbook saves the next person the entire investigation. The general habit of reading which component produced the error rather than only the status code applies well beyond uploads.
For anything above about 100 MB, stop proxying it. Issue a presigned URL and let the client upload straight to object storage. Every limit in this post disappears, and so does the memory pressure of buffering large bodies through your own infrastructure.