Why does nginx return 413 Request Entity Too Large?
The body is over client_max_body_size (1m by default) in the location that serves it. Why raising it often fails, and which layer really sent your 413.
Key points
- nginx returns 413 when the request body is larger than
client_max_body_size, which defaults to1m. The limit is inclusive: a body of exactly 1,048,576 bytes passed and 1,048,577 bytes got a 413, on nginx 1.27.5 and 1.30.6. It logsclient intended to send too large body: N bytesaterror, so unlike most nginx refusals it is in the default error log. - The value that counts is the one in the location that finally serves the request. A regex location such as
~ \.php$wins over the prefix location where you raised the limit, andrewrite ... lastinto another location applies that location's limit. Both gave 413 with a 10m limit in the location the URL started in. - nginx is often not the layer that sent the 413. The response tells you: nginx sends
Server: nginxand logs$upstream_statusas-; a 413 from the application arrives with an upstream status of 413. Express (100kb default) saysPayload Too Large, Envoy and HAProxy too, Traefik sends 24 bytes of plain text, Caddy an empty body. - Some layers never send a 413 at all. PHP over
post_max_size(8M) answered 200 with empty$_POSTand$_FILES; Django overDATA_UPLOAD_MAX_MEMORY_SIZE(2.5 MB) answered 400. Apache 2.4.69 did not applyLimitRequestBodyto proxied requests. - With
proxy_request_buffering offand a chunked body, the backend received about 1 MB, then an aborted connection, before nginx sent its 413. A client still uploading after nginx answered was reset at about 30 seconds, which islingering_time.
nginx returns 413 Request Entity Too Large when a request body is bigger than client_max_body_size, which is 1m (1,048,576 bytes) unless you set it. The fix is to raise it in the location that actually handles the upload, then reload:
location /upload/ {
client_max_body_size 50m; # 0 disables the check
proxy_pass http://app;
}When that does not work, there are two usual reasons. Either the request is served by a different location from the one you edited, or the 413 did not come from nginx at all: an Ingress controller, a CDN or the application has its own limit, and some of them do not even answer 413. The error log tells you which. nginx's own refusal is logged at error level, so it is in the default log:
[error] 30#30: *3 client intended to send too large body: 1048577 bytes, client: 10.201.13.1, server: , request: "POST / HTTP/1.1", host: "10.201.13.3"No such line next to the 413 in the access log means another layer sent it.
| What you see | Who sent it | Where to change the limit |
|---|---|---|
413, Server: nginx, error log client intended to send too large body | nginx | client_max_body_size in the serving location |
| Same, from a Kubernetes Ingress | ingress-nginx (default 1m) | nginx.ingress.kubernetes.io/proxy-body-size annotation or the proxy-body-size ConfigMap key |
413 passed through nginx, $upstream_status is 413 | The application | Its framework limit, for example Express limit (100kb by default) |
413 with server: envoy, body Payload Too Large | Envoy buffer filter | max_request_bytes |
413, 24-byte body Request Entity Too Large, no Server header | Traefik Buffering middleware | maxRequestBodyBytes |
| 413 from Caddy with an empty body | Caddy request_body | max_size (decimal units: 1MB is 1,000,000 bytes) |
| 200, but the form fields and files are empty | PHP over post_max_size (8M) | post_max_size and upload_max_filesize |
| 400 Bad Request on a large form post | Django over DATA_UPLOAD_MAX_MEMORY_SIZE (2.5 MB) | That setting |
| 413 before your origin is reached, behind Cloudflare | Cloudflare (100 MB on Free and Pro) | The plan, or a DNS-only record for the upload host |
How the cases were reproduced#
Everything below was observed on 21 October 2026 unless it is attributed to documentation or source code. nginx 1.30.6 and 1.27.5 ran from the official Alpine images, with identical results, in front of a Node 24 server that counted the bytes it received. The same uploads then went through HAProxy 3.2.26, Caddy 2.11.7, Traefik 3.7.15, Envoy 1.36.13 and Apache httpd 2.4.69 with mod_proxy. PHP-FPM 8.4.26, Flask 3.1.3 on Werkzeug 3.1.9, Django 6.1.2 and Express 5.3.0 (body-parser 2.3.0) were tested as backends. The client was curl 7.68.0, plus a Python socket script for the case where the client keeps writing. The ingress-nginx and Cloudflare rows come from their documentation and were not run.
What nginx checks, and when#
The core module documentation says it in one paragraph: client_max_body_size "sets the maximum allowed size of the client request body. If the size in a request exceeds the configured value, the 413 (Request Entity Too Large) error is returned to the client." It is allowed in http, server and location, and "setting size to 0 disables checking of client request body size."
Three details decide how it behaves in practice.
The limit is inclusive and binary. 1m means 1,048,576 bytes. A body of exactly that size passed through to the backend; one byte more was refused. The k and m suffixes are multiples of 1024.
The check runs after the location is chosen, against Content-Length. In the nginx source the test sits in ngx_http_core_find_config_phase(), right after the location configuration is applied, and compares the declared length with the location's client_max_body_size. So nginx refuses before it reads the body and before it contacts the upstream: the access log shows $upstream_status as -, and curl had sent only 64 to 128 KiB of a 5 MB body when the response arrived. Because the phase runs again whenever nginx searches for a location, the check repeats after an internal redirect.
A body with no length is counted as it arrives. For a chunked HTTP/1.1 body, nginx adds up the chunks while reading and stops once the total would pass the limit. The log line is different:
[error] 43#43: *18 client intended to send too large chunked body: 1048384+65524 bytes, client: 10.201.13.1, server: , request: "POST / HTTP/1.1", host: "10.201.13.3"The first number is what had been accepted so far, the second the chunk that would have crossed the limit. HTTP/2 requests behave the same way. With a Content-Length they got the plain too large body line and a 413 after 64 KiB; without one, nginx logged client intended to send too large chunked body: 1056768 bytes even though HTTP/2 has no chunked encoding. A body of exactly 1,048,576 bytes passed over HTTP/2 as well.
Every 413 from nginx carries Connection: close and a fixed 183-byte HTML page whose title is 413 Request Entity Too Large. RFC 9110 renamed the status to 413 Content Too Large; nginx and Traefik still use the old reason phrase, while HAProxy, Envoy and Node send Payload Too Large. The reason phrase is one more clue to who answered.
Why raising client_max_body_size seems not to work#
The usual report is "I set client_max_body_size 100m and still get 413." In testing, nginx always applied the value from the location that served the request. The surprises come from which location that is.
| Configuration | Request | Result with a 5 MB body |
|---|---|---|
location /big/ { client_max_body_size 10m; } | POST /big/ | 200 |
Same, plus location ~ \.upl$ { proxy_pass ...; } with no limit | POST /big2/x.upl | 413: the regex location wins and has the 1m default |
location /rw/ { client_max_body_size 10m; rewrite ^/rw/(.*)$ /small/$1 last; } | POST /rw/x | 413: logged as /small/x, the limit of /small/ applies |
location /up/ { client_max_body_size 10m; location ~ \.bin$ { ... } } | POST /up/a.bin | 200: a nested location inherits the outer value |
client_max_body_size 0; in the location | POST /off/ | 200 |
The regex case is the common one with PHP. A server block that raises the limit in location / and passes .php files through location ~ \.php$ checks every PHP upload against the regex location's value. Put the directive at server level, or in the location that names the script. The location matching order that causes this is set out in nginx as a reverse proxy: a regex beats a longer prefix.
The other causes are outside the configuration you edited. A change that is never loaded does nothing, so check with nginx -T | grep client_max_body_size against the running master. Behind Kubernetes, the controller renders its own configuration: ingress-nginx's ConfigMap key proxy-body-size defaults to "1m", and the per-Ingress annotation nginx.ingress.kubernetes.io/proxy-body-size overrides it. ingress-nginx is retired; how that annotation maps to Gateway API, and the envoy-gateway emitter turning it into a hard requestBuffer.limit, is covered in what replaces the ingress-nginx annotations.
Custom error pages and JSON for API clients#
An API client usually wants JSON, not nginx's HTML page. error_page 413 works for this, because nginx finalizes the request with a 413 after the location is chosen:
location /api/ {
error_page 413 = @too_large;
proxy_pass http://app;
}
location @too_large {
default_type application/json;
return 413 '{"error":"body too large","limit":"1m"}\n';
}The client received status 413 and that JSON body. The URI form, error_page 413 /413.json; pointing at an internal location, also kept the 413 status even though that location used return 200, and the access log recorded the request as GET /413.json, which matters if you count uploads by URI. Put error_page in the same location as the limit or at server level. As with every error_page, one defined at a lower level replaces those inherited from above.
Expect: 100-continue, and clients that keep sending#
The nginx documentation adds a warning after the directive: "Please be aware that browsers cannot correctly display this error." The tests show where the difficulty comes from.
A client that asks first loses nothing. curl sent Expect: 100-continue with the 5 MB upload, and nginx answered 413 in place of 100 Continue, so the body was never transmitted. This is the behaviour RFC 9110 designs for: the server "MAY terminate the request, if the protocol version in use allows it; otherwise, the server MAY close the connection."
A client that sends without asking gets the 413 early, then a closing connection. nginx sends the response as soon as it has the headers and then reads and discards what is still coming, under lingering_close (default on). That stops after lingering_time, default 30s. The socket script uploaded at 1 MB/s without reading. With a 20 MB body it finished in 20 seconds and then read the 413. With a 100 MB body, the write failed with Connection reset by peer after 29.7 seconds and 31 MB. The 413 was still in its receive buffer, but a client that treats the failed write as fatal never looks at it. curl avoided this by watching for a response while sending: it stopped uploading and reported the 413.
So a large, slow upload over the limit can end as a reset connection rather than a readable error, and the client shows a network failure. Raising lingering_time gives slow clients longer to finish; it does not change the 413. The better fix is a client-side size check before the upload starts. A client that gives up partway through an upload within the limit is a different event, logged as 400 rather than 499, as described in what nginx 499 means.
Streaming uploads: the backend sees part of the body#
With the default proxy_request_buffering on, nginx reads the whole body before it contacts the upstream, so a 413 never touches the backend. With proxy_request_buffering off, which is set for large or streaming uploads, it depends on whether the size was declared:
| Body | proxy_request_buffering | What the backend saw |
|---|---|---|
5 MB with Content-Length | on or off | Nothing: 413 before any upstream connection |
| 5 MB chunked | on | Nothing |
| 5 MB chunked | off | 1,040,258 bytes, then an aborted connection |
In the last case nginx had already opened the upstream connection and forwarded what it had read, and the error log said so: client intended to send too large chunked body: 1048384+65524 bytes while sending request to upstream. The client still got a 413, but the backend saw a request start and then fail partway through the body. An application that writes uploads straight to storage needs to clean up after an aborted request in any case; with streaming on, an over-limit chunked upload is one more way to produce one.
Caddy's request_body behaves like this on every request, as shown in the next section.
The same limit at every other layer#
Each hop that handles the body can refuse it, and the defaults differ by orders of magnitude. All five proxies passed a 100 MB body with their default configuration. With a limit configured, they did not all refuse it the same way.
| Layer | Default limit | Setting | Over the limit (tested) |
|---|---|---|---|
| nginx 1.27.5, 1.30.6 | 1m | client_max_body_size | 413 before any upstream contact (Content-Length) |
| ingress-nginx | 1m | proxy-body-size | nginx's 413 (documentation) |
| Caddy 2.11.7 | none | request_body { max_size 1MB } | 413, empty body; backend received exactly 1,000,000 bytes, then an abort |
| Traefik 3.7.15 | none | Buffering maxRequestBodyBytes | 413 Request Entity Too Large; backend saw nothing |
| HAProxy 3.2.26 | none | http-request deny deny_status 413 if { req.hdr_val(content-length) gt N } | 413 Payload Too Large, termination state PR--; chunked bodies passed |
| Envoy 1.36.13 | none on a streamed route | Buffer filter max_request_bytes | 413 Payload Too Large; backend saw nothing |
| Apache 2.4.69 mod_proxy | 1 GiB (2.4.54 and later) | LimitRequestBody | Not applied: a 5 MB body passed a 1 MiB limit |
| Cloudflare | 100 MB Free and Pro, 200 MB Business, up to 5 GB Enterprise | Plan, zone Network settings | 413 (documentation) |
Caddy checks while reading, not up front. Its documentation says reads of more bytes than max_size "will return an error with HTTP status 413", and that is what happened. With a 100 MB body declared in Content-Length, Caddy still opened the upstream request and streamed the first 1,000,000 bytes before cutting it off. 1MB there is decimal, because the value is parsed by go-humanize; write 1MiB for 1,048,576. The directive is marked experimental. Its other defaults are on the page about Caddy as a reverse proxy.
HAProxy has no body size directive. The ACL on Content-Length refused declared bodies before the server was chosen (<NOSRV> in the log). A 5 MB chunked body went straight through it, and also through a second rule using req.body_size with option http-buffer-request and wait-for-body. The manual defines req.body_size for chunked requests as "the size of the available data", which is only what fits in the buffer, not the whole body. To cap chunked uploads, put the limit on a layer that counts the stream.
Traefik and Envoy buffer to enforce. Traefik's Buffering middleware documentation says an oversized request "is not forwarded to the Service". Envoy's buffer filter is the same: max_request_bytes is "the maximum request size that the filter will buffer before the connection manager will stop buffering and return a 413 response". Both hold the whole body in memory or on disk first, so they are not free to turn on for large uploads. Where they sit in a configuration is covered in Traefik routers, services and middlewares and Envoy listeners, routes and clusters.
Apache does not limit proxied bodies. The core documentation gives LimitRequestBody a default of 1073741824 since 2.4.54 (it was 0, unlimited, in 2.4.53 and earlier). Set to 1 MiB at server level and in a <Location>, it stopped nothing that mod_proxy forwarded. The reason is in modules/http/http_filters.c on the 2.4.x branch: the HTTP input filter takes its limit from LimitRequestBody only if (!f->r->proxyreq), and uses 0 otherwise. The comment above that line talks about proxied responses, but a request handled by ProxyPass has proxyreq set too, so its body is not limited either.
Application limits that do not say 413#
The application behind the proxy often has a smaller limit than either proxy, and only some frameworks report it as a 413.
| Backend | Default | Over the limit |
|---|---|---|
| Express 5.3.0, body-parser 2.3.0 | limit 100kb for json, urlencoded, raw and text | 413 Payload Too Large, error type entity.too.large |
| Flask 3.1.3 | MAX_CONTENT_LENGTH None; MAX_FORM_MEMORY_SIZE 500,000 | 413 only for a non-file multipart field over 500,000 bytes; a 100 MB raw body and a 3 MB urlencoded form passed |
| Django 6.1.2 | DATA_UPLOAD_MAX_MEMORY_SIZE 2,621,440 | 400 for a 3 MB urlencoded form and for request.body of 3 MB; a 10 MB multipart file passed |
| PHP-FPM 8.4.26 | post_max_size 8M, upload_max_filesize 2M | 200, see below |
PHP is the case that sends people to the nginx configuration for the wrong reason. Over post_max_size, the script still ran and returned 200, with $_POST and $_FILES empty and a startup warning: PHP Request Startup: POST Content-Length of 10000285 bytes exceeds the limit of 8388608 bytes. The PHP manual says so: "If the size of post data is greater than post_max_size, the $_POST and $_FILES superglobals are empty." A 3 MB file under post_max_size but over upload_max_filesize arrived with error 1 (UPLOAD_ERR_INI_SIZE) and size 0, again with status 200. The application sees a form submitted with nothing in it.
Django's 400 is its RequestDataTooBig, a SuspiciousOperation. Its documentation says that for request.POST the check "uses the total request size without the file upload data", which is why the 10 MB file passed and the 3 MB text form did not.
When nginx proxies to one of these, nginx's own log shows the status the application chose, with $upstream_status set. A 413 logged with $upstream_status 413 was the application's; a 413 with - was nginx's. Express's 413 arrived through nginx with X-Powered-By: Express intact.
Choosing the numbers#
Set the proxy limit to the largest body the endpoint legitimately accepts, per location, and set every layer behind it at least as high. A higher limit in front only moves the refusal to a layer with a worse error. For an upload endpoint behind nginx and PHP, that means client_max_body_size, post_max_size and upload_max_filesize together, with post_max_size above upload_max_filesize as the PHP manual requires. For a JSON API behind Express, the 100kb default is usually the one that bites first.
Keep a small default everywhere else. The 1m default protects locations that never expect a body, and large uploads also need timeouts that cover the transfer: a body that is allowed but stalls for longer than client_body_timeout (60s between two reads) fails with a 408 instead. Request header limits are a separate set of buffers with their own status codes, covered in header and body size limits at the proxy.
Failure modes#
413 on uploads to .php scripts after raising the limit in location /. The ~ \.php$ location serves the request and has the default. Move client_max_body_size to server level.
413 logged with a URI you did not request. A rewrite ... last sent the request to another location, whose limit applied. Raise it there.
413 with no client intended to send too large body line in the error log. Another layer sent it. Check $upstream_status, the Server header and the reason phrase against the table at the top.
Uploads fail with a connection reset, not a 413. The body is over the limit and the upload took longer than lingering_time (30s) after nginx answered. Check the error log for the too large body line.
The form arrives empty, status 200. PHP's post_max_size was exceeded. Raise it with upload_max_filesize.
A large form post returns 400 Bad Request from Django. DATA_UPLOAD_MAX_MEMORY_SIZE was exceeded.
The backend logs aborted uploads that the client saw as 413. proxy_request_buffering off with a chunked body, or Caddy's request_body, forwarded part of the body before the limit was hit.
Apache in front still forwards huge bodies. LimitRequestBody does not apply to ProxyPass requests on 2.4.69. Limit the body on another hop.
Where this answer stops applying#
The nginx results were identical on 1.27.5 and 1.30.6 over HTTP/1.1 and HTTP/2; HTTP/3 was not tested. The slow client results are for one client that writes without reading; real browsers were not tested, so the wording of the error they show is not stated here. Proxy and framework results are for the versions listed, with the stated configuration; Caddy's request_body is marked experimental and may change. ingress-nginx, Cloudflare and other managed load balancers were not run, and their limits are as their documentation states on 21 October 2026. For a systematic way to find which hop refused a request, the proxy debugging playbook bisects the chain, and curl through a proxy shows how to send the same body to each hop directly.
Frequently asked questions#
What is the default client_max_body_size in nginx?#
The default is 1m, which is 1,048,576 bytes. A body of exactly that size is accepted; one byte more gets a 413. Setting it to 0 disables the check.
Where should I put client_max_body_size?#
Put it in the location that serves the upload, or at server or http level so every location inherits it. A value in a prefix location does not apply when a regex location such as ~ \.php$ handles the request, or after a rewrite sends it to another location.
Why do I still get 413 after increasing client_max_body_size?#
Usually because another location serves the request, the configuration was not reloaded, or another layer sent the 413. If the nginx error log has no client intended to send too large body line for the request, check the Ingress controller, the CDN and the application.
How do I tell whether nginx or my application returned the 413?#
Log $upstream_status. nginx's own 413 never contacts the upstream, so the value is -, and the error log has a too large body line. A 413 from the application is logged with $upstream_status 413 and usually carries its own reason phrase, such as Payload Too Large.
What does "client intended to send too large chunked body" mean?#
The request had no Content-Length, so nginx counted the body as it arrived and stopped when the next chunk would pass client_max_body_size. The first number is what it had accepted, the second the chunk that crossed the limit. HTTP/2 requests without a length are logged with the same wording.
How do I increase the upload size on a Kubernetes ingress-nginx Ingress?#
Set the annotation nginx.ingress.kubernetes.io/proxy-body-size, for example to 50m, on the Ingress, or the proxy-body-size key in the controller ConfigMap for every Ingress. The default is 1m.
Why is $_POST empty for large uploads in PHP behind nginx?#
The body was larger than PHP's post_max_size (8M by default). PHP then runs the script with empty $_POST and $_FILES and returns 200 rather than 413. Raise post_max_size and upload_max_filesize along with client_max_body_size.
Primary sources#
Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.
- nginx ngx_http_core_module (client_max_body_size, client_body_buffer_size, lingering_close, lingering_time, lingering_timeout)
- nginx source, ngx_http_core_module.c (ngx_http_core_find_config_phase, body size check)
- nginx source, ngx_http_request_body.c (too large chunked body)
- RFC 9110, HTTP Semantics, section 15.5.14 (413 Content Too Large)
- ingress-nginx annotations (proxy-body-size) and ConfigMap default
- Caddy documentation, request_body directive
- Traefik Buffering middleware (maxRequestBodyBytes)
- Envoy buffer filter API (max_request_bytes)
- HAProxy 3.2 configuration manual (req.body_size, http-request deny)
- Apache httpd core (LimitRequestBody) and source http_filters.c (2.4.x)
- PHP manual, core php.ini directives (post_max_size, upload_max_filesize)
- Flask configuration (MAX_CONTENT_LENGTH, MAX_FORM_MEMORY_SIZE)
- Django settings (DATA_UPLOAD_MAX_MEMORY_SIZE)
- body-parser README (limit, default 100kb)
- Cloudflare, Error 413 (maximum upload size by plan)
Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.
More in troubleshooting proxies#
- 502 vs 503 vs 504: reading proxy errors correctly
- Cntlm vs Px: which one for an NTLM or Kerberos proxy?
- Corporate proxies and developer tooling
- Access-Control-Allow-Origin multiple values: the fix
- curl through a proxy
- HTTPS_PROXY: http:// or https://? Tested in 12 clients
- netsh winhttp vs Windows proxy settings: who reads which?
- nginx 499: what it means, and its ALB, HAProxy, Envoy names
- nginx 502 after Docker restart: fix the stale IP
- nginx drops headers with underscores: why, and the fix
- nginx 400 plain HTTP request sent to HTTPS port: fix
- nginx connect() failed to [::1] with localhost: the fix
- nginx SSL handshaking to upstream errors: fix each one
- nginx cuts off large downloads: find which of 5 causes
- nginx upstream sent no valid HTTP/1.0 header: the fix
- nginx worker_connections: the right number for a proxy
- The no_proxy environment variable
- A proxy debugging playbook