> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# 413 Request Entity Too Large When You Already Raised the Limit
- URL: https://debugly.dev/413-request-entity-too-large-two-limits/
- Published: 2026-08-22T13:00:00.000Z
- Updated: 2026-10-23T09:13:11.000Z
- Description: 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.
- Author: Rohit Bhadani
- Tags: MicroVMs, Error Autopsy, Networking, Web Development

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

```bash
curl -i -X POST https://app.example.com/upload \
  -F "file=@big.bin" \
  -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.

```nginx
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](https://krova.cloud/?ref=debugly.dev) 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.

```js
// 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](https://debugly.dev/how-to-read-python-traceback/) 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.

---

Related reading: [The CORS Header Is Present Twice and That Is Why It Fails](https://debugly.dev/cors-duplicate-headers-proxy-and-app/) explores a connected angle.

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)