What 413 means

413 Payload Too Large (RFC 9110 now calls it 413 Content Too Large; nginx and many others still print 413 Request Entity Too Large) means one thing: something between the client and your code measured the request body and decided it was bigger than it’s willing to accept.

Nothing is wrong with the data, the login or the URL. It’s a size limit, and the only real question is which limit. A typical upload passes through a CDN, maybe a load balancer, a reverse proxy, a language runtime and a framework. Each has its own body limit, configured separately, and the first one that says no produces the error. Raising the limit in your app does nothing if nginx in front of it already said no.

So the work is: find the threshold, find the layer, change the right setting.

If you’re uploading to someone else’s site

The file is bigger than that site accepts. You can’t change their limit. Make the file smaller: resize or recompress the photo, export the video at a lower resolution, split a large archive, or check the site’s help pages for its stated maximum. If even small files fail, the site is misconfigured and the owner needs to know.

Step 1: find the size where it breaks

The threshold usually names the culprit, so measure it. Make test files of known sizes and send them the same way the real upload does:

for size in 900K 2M 10M 50M 120M; do
  head -c $size /dev/urandom > /tmp/test-$size.bin
  printf '%s ' $size
  curl -s -o /dev/null -w '%{http_code}\n' \
    -F "file=@/tmp/test-$size.bin" https://example.com/upload
done
900K 200
2M 413
10M 413
50M 413
120M 413

Use --data-binary @file instead of -F if the real client sends a raw body (a JSON API, a PUT to object storage). Run it against an endpoint where a test upload is harmless.

Rough guide to what the number points at:

Breaks just aboveUsual suspect
100 KB (JSON body)Express express.json() default limit
1 MBnginx client_max_body_size default, or an ingress-nginx default
2 MB / 8 MB (no 413, upload just “fails”)PHP upload_max_filesize / post_max_size defaults
about 28.6 MB (30,000,000 bytes)IIS request filtering or Kestrel default
100 MBCloudflare Free or Pro

Step 2: find which layer answered

Look at the full 413 response, not just the code:

curl -si -F "file=@/tmp/test-2M.bin" https://example.com/upload | head -20
  • Server: nginx with a short HTML page “413 Request Entity Too Large”: nginx. Check the error log on each nginx instance in the path for client intended to send too large body: 2097364 bytes. That line tells you which one rejected it.
  • A Cloudflare-branded page, server: cloudflare, and nothing in your origin’s access log for that request: the Cloudflare edge.
  • JSON like {"message":"request entity too large"} or your framework’s error format: the application.
  • HTTP Error 404.13 from IIS: request filtering (more below).

Then bypass the outer layers and test the origin directly. --resolve sends the request to the origin IP while keeping the right hostname:

curl -s -o /dev/null -w '%{http_code}\n' \
  --resolve example.com:443:203.0.113.10 \
  -F "file=@/tmp/test-120M.bin" https://example.com/upload

If the origin accepts a size that fails through the public hostname, the limit is in front of it. The DNS lookup shows whether the hostname resolves to a CDN, and the HTTP check shows the server and CDN headers on the public side.

The layers, one by one

nginx: client_max_body_size

The most common 413 by a wide margin. The default is 1 MB, and nginx rejects larger bodies before passing anything upstream. nginx’s own documentation warns that browsers can’t always display this error properly, which is why users often report a stalled upload rather than a 413.

server {
    client_max_body_size 50m;

    location /upload {
        client_max_body_size 200m;
        proxy_pass http://app;
    }
}

It can be set in http, server or location, and the most specific block wins. A common trap is raising it in http {} while an included server or location file sets a smaller value. Find every occurrence of the effective config:

sudo nginx -T 2>/dev/null | grep -n client_max_body_size
sudo nginx -t && sudo systemctl reload nginx

client_max_body_size 0; disables the check. Better to set a real ceiling a bit above your largest legitimate upload.

If there are two nginx instances (a load balancer and another nginx in the app container, say), both need the change. On Kubernetes with ingress-nginx, the per-Ingress setting is the annotation nginx.ingress.kubernetes.io/proxy-body-size: "50m".

Apache: LimitRequestBody

Apache limits bodies with LimitRequestBody, in bytes, settable per server, directory or .htaccess. For years the default was unlimited; recent 2.4 releases ship with a 1 GiB default. If you run ModSecurity, its SecRequestBodyLimit is a separate ceiling that also returns 413, and the commonly used recommended config sets it to roughly 12.5 MB, so check that too.

<Location "/upload">
    LimitRequestBody 209715200
</Location>

PHP: upload_max_filesize and post_max_size

PHP has two limits, defaulting to 2 MB and 8 MB. upload_max_filesize caps each file; post_max_size caps the whole body and should be larger.

What surprises people is that PHP doesn’t answer with 413. Over post_max_size, the script still runs, but $_POST and $_FILES are empty and the log shows PHP Warning: POST Content-Length of … bytes exceeds the limit of … bytes. Over upload_max_filesize, the file arrives with error code UPLOAD_ERR_INI_SIZE; WordPress shows that as “The uploaded file exceeds the upload_max_filesize directive in php.ini”. If you’re looking at a real 413 on a PHP site, it came from nginx, Apache or the CDN in front.

Set both in the php.ini that PHP-FPM actually loads, then restart PHP-FPM. The CLI often reads a different file, so php -i can show values the web server doesn’t use. A temporary phpinfo() page served through the web server shows the real ones (delete it afterwards). On shared hosting, both settings can usually go in .user.ini (FPM) or .htaccess with php_value (mod_php).

Application frameworks

  • Express: express.json() and express.urlencoded() default to 100 KB and answer 413 with PayloadTooLargeError: request entity too large. Pass { limit: '10mb' } for the routes that need it.
  • ASP.NET Core on Kestrel: MaxRequestBodySize defaults to 30,000,000 bytes and returns 413. Raise it globally or per action with [RequestSizeLimit].
  • Spring Boot: spring.servlet.multipart.max-file-size (default 1 MB) and max-request-size (default 10 MB). The error status depends on how your app handles the exception.
  • Django: request bodies other than file uploads over DATA_UPLOAD_MAX_MEMORY_SIZE (default 2.5 MB) are rejected with a 400, not a 413. Worth knowing when a large JSON post fails with a vague bad request.

IIS: 404.13 instead of 413

IIS request filtering limits bodies to 30,000,000 bytes (about 28.6 MB) by default and reports it as HTTP Error 404.13 - Not Found (CONTENT_LENGTH_TOO_LARGE). It’s the same problem with a misleading code. Classic ASP.NET adds maxRequestLength, in kilobytes, default 4096 (4 MB). Raise both:

<system.web>
  <httpRuntime maxRequestLength="204800" />
</system.web>
<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="209715200" />
    </requestFiltering>
  </security>
</system.webServer>

Note the units: kilobytes in the first, bytes in the second.

When the limit belongs to your CDN or platform

Some ceilings you can’t raise from your config.

Cloudflare. Proxied requests are limited by plan. Cloudflare’s documentation lists 100 MB for Free and Pro, 200 MB for Business and 500 MB by default for Enterprise; check their current limits page, since plans change. Over the limit, the edge returns 413 and your origin never sees the request.

Serverless and gateways. AWS documents a 10 MB payload limit for API Gateway and 6 MB for synchronous Lambda invocations. Vercel Functions have a request body limit of about 4.5 MB. Netlify, Cloud Run and others have their own; look up the current numbers for your platform.

The fix for these is architectural, not a setting:

  • Upload straight to object storage. Your API issues a pre-signed URL (S3, R2, GCS), the browser PUTs the file there, and your app only receives a small JSON body with the object key. This is the right design for large files anyway.
  • Chunked uploads. Split the file client-side into parts under the limit (S3 multipart, or a resumable protocol such as tus) and reassemble on the server or in storage.
  • A separate upload hostname that isn’t proxied (DNS only on Cloudflare). It works, but it exposes your origin IP and skips the CDN’s protections for that hostname. Lock that endpoint down accordingly.

413 isn’t the only way big requests fail

  • Timeouts. A large upload on a slow connection can take longer than a proxy or CDN waits. That shows up as 408, 504 or a Cloudflare 524, not 413, and the fix is timeouts or chunking, not size limits.
  • Headers, not body. A tiny request with huge cookies or a very long URL gets 431, 414 or nginx’s “400 Request Header Or Cookie Too Large”. That’s a header limit; client_max_body_size won’t help.
  • The 413 arrives before the upload finishes. When the client announces the size up front (Content-Length, or Expect: 100-continue), the server can reject immediately. If it closes the connection while the browser is still sending, the browser may show a reset or a generic upload failure. curl shows the real status.

After the fix

  • Rerun the size loop from step 1. Every size up to your intended maximum should succeed, and the next size up should fail where you expect it to, not at some layer you forgot.
  • Repeat through the public hostname, not just against the origin.
  • Tell users the limit in the upload form. A clear “maximum 50 MB” saves more support time than any server setting.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.