CoreMesh

Nginx 413 Request Entity Too Large: Fix and Verify With curl

Fix nginx 413 Request Entity Too Large by setting client_max_body_size at the right level, reloading safely, and verifying the upload with curl.

Nginx 413 Request Entity Too Large: Fix and Verify With curl

Nginx sends 413 Request Entity Too Large when the request body is bigger than the limit set by client_max_body_size. The fix is to raise that directive in the context that handles the upload, reload nginx, and prove the new limit with a test upload from curl.

The shortest working fix goes inside the server block that receives the upload:

nginx
server {
    listen 80;
    server_name example.com;
    client_max_body_size 20m;

    location / {
        proxy_pass http://localhost:8080;
    }
}

Then run nginx -s reload. If that solved it, you're done. The rest covers which level to put the directive at, why a reload can silently keep the old limit, and the second limit in your Node, Python, or PHP app that tends to fail next.

What the 413 means and who is sending it#

The 413 comes from nginx itself, not from the application behind it. Nginx reads the request headers, decides the body is over the limit, and answers with 413 before anything reaches your backend. That's why the app's logs show nothing for the failed upload: the request never got there.

The directive that sets the limit is client_max_body_size, documented in the ngx_http_core_module reference.

Two neighbors in the same module cause confusion. client_body_buffer_size is about buffering the body in memory, and client_body_temp_path is about the temporary file it spills into. Neither one is the limit. Raising them doesn't change a 413.

Where to set client_max_body_size: http, server, or location#

Nginx config is nested. The beginner's guide describes the layout: directives outside any block are in the main context, http sits in main, server sits inside http, and location sits inside server. A directive set in an outer block applies to the inner blocks unless an inner block sets its own value. The guide shows this with root, and client_max_body_size behaves the same way.

So you have three places to put it:

Level Applies to Use it when
http Every server and location in the file You want one site-wide ceiling
server One virtual host and all its locations One site accepts uploads, others don't
location Only requests that match that location A single upload endpoint needs a big limit

Here's all three together, each one overriding the one above it:

nginx
http {
    client_max_body_size 2m;

    server {
        listen 80;
        server_name example.com;
        client_max_body_size 10m;

        location / {
            proxy_pass http://localhost:8080;
        }

        location /api/upload {
            client_max_body_size 200m;
            proxy_pass http://localhost:8080;
        }
    }
}

A request to /api/upload/photo is allowed 200m. Any other path on example.com gets 10m. A second server block in the same file, with no value of its own, inherits 2m from http.

My advice: put the big number on the upload location and keep the http and server values modest. A site-wide 500m means every endpoint, including the login form, will accept a 500m body and hold it while your backend works out what to do with it. The limit is also a cheap guard against junk traffic, which is the same reasoning behind rate limiting in nginx.

Location matching decides which value wins, and it isn't "first match". Nginx picks the longest matching prefix location, then checks regular-expression locations, and a matching regex location beats the prefix. If your upload URL is routed by a regex location, a value set in location / is never consulted. Set it in the regex location or at the server level.

The script below walks through the resolution rule with a few combinations, so you can sanity check your own layering before touching the real file.

javascript
// Which client_max_body_size wins? The innermost context that sets it.
const UNITS = { k: 1024, m: 1024 * 1024 };

function toBytes(size) {
  const m = /^(\d+)([km])?$/i.exec(String(size).trim());
  if (!m) throw new Error("bad size: " + size);
  return Number(m[1]) * (m[2] ? UNITS[m[2].toLowerCase()] : 1);
}

function effectiveLimit(levels) {
  for (const ctx of ["location", "server", "http"]) {
    if (levels[ctx] !== undefined) return { ctx, bytes: toBytes(levels[ctx]) };
  }
  return { ctx: "built-in default", bytes: null };
}

const upload = 25 * 1024 * 1024; // a 25 MiB file
const cases = [
  ["http only", { http: "2m" }],
  ["server overrides http", { http: "2m", server: "10m" }],
  ["location wins", { http: "2m", server: "10m", location: "200m" }],
  ["nothing set", {}],
];

for (const [name, levels] of cases) {
  const { ctx, bytes } = effectiveLimit(levels);
  let verdict;
  if (bytes === null) verdict = "default applies, see the directive reference";
  else verdict = upload <= bytes ? "passes" : "413";
  console.log(`${name.padEnd(22)} limit from ${ctx.padEnd(16)} ${String(bytes ?? "?").padStart(10)} bytes  25 MiB upload: ${verdict}`);
}

Reload nginx without silently keeping the old limit#

Editing the file changes nothing until nginx reloads. Send the reload signal:

bash
nginx -s reload

Per the beginner's guide, the master process checks the syntax of the new file, starts new workers if it's valid, and tells the old workers to finish their current requests and exit. In-flight uploads complete on the old workers. If the new file has a mistake, the master rolls back and keeps serving the old configuration.

That rollback is the safe part, and also the trap. A reload that fails doesn't stop nginx and doesn't change what clients see. The 413 keeps coming, and it looks like your edit "didn't work". After every reload, check error.log in /usr/local/nginx/logs or /var/log/nginx for a parse error.

Two more details from the same page. Run the reload as the same user that started nginx, or the signal won't reach the master. And make sure you're editing the file nginx actually loaded. The default name is nginx.conf, and depending on the build it lives in /usr/local/nginx/conf, /etc/nginx, or /usr/local/etc/nginx. Some packaged installs split the configuration into several files pulled in by the main one, so the directive needs to land in a file that's part of the loaded set.

Verify the fix with a curl upload#

Don't trust the config, test it. Make two files, one under the new limit and one over it, then POST both.

bash
# with client_max_body_size 20m: 15 MiB should pass, 25 MiB should get 413
dd if=/dev/zero of=under.bin bs=1M count=15
dd if=/dev/zero of=over.bin bs=1M count=25

curl -s -o /dev/null -w '%{http_code}\n' --data-binary @under.bin http://example.com/api/upload
curl -s -o /dev/null -w '%{http_code}\n' --data-binary @over.bin http://example.com/api/upload

The first request should return whatever your app returns for a body of zeros, probably a 400 or a 422 because it isn't a real file. That's fine. The point is that it is not a 413, which proves nginx passed it through. The second should print 413. If the first one also prints 413, the request is hitting a context that didn't get the new value, or the reload rolled back.

For a multipart form upload, swap --data-binary @under.bin for -F [email protected]. Both forms and the -w output variables are described in the curl manual. Add -i to see the response headers. The Server header tells you which hop answered, which matters once there's more than one proxy in the path.

Match the limit in a proxied Node or Python app#

Once nginx lets a body through, your app sees it, and most frameworks have a body-size cap of their own. If that cap is lower than nginx's, the upload fails one layer later, often with a 413 from the app, which looks exactly like the error you just fixed. Set the app's limit to the same value as the nginx location that fronts it. Equal values mean the error always comes from one place with one message.

Your framework's setting is the right place for this. If you want an explicit guard that doesn't depend on framework defaults, a Content-Length check at the top of the handler is enough for ordinary uploads. Here's the Node version, with the limit written in bytes so it matches 20m:

javascript
// Minimal body-size guard for a plain Node http handler.
// Mirror the nginx value so both layers agree.
const LIMIT = 20 * 1024 * 1024; // 20m in nginx terms

function bodyTooLarge(headers, limit = LIMIT) {
  const len = Number(headers["content-length"]);
  if (!Number.isFinite(len)) return false; // no length: enforce while streaming
  return len > limit;
}

// In a real server:
// if (bodyTooLarge(req.headers)) { res.writeHead(413); return res.end(); }
const samples = [
  { "content-length": String(5 * 1024 * 1024) },
  { "content-length": String(20 * 1024 * 1024) },
  { "content-length": String(20 * 1024 * 1024 + 1) },
  {},
];

for (const h of samples) {
  const shown = h["content-length"] ?? "(none)";
  console.log(`Content-Length ${shown.padStart(9)} -> ${bodyTooLarge(h) ? "413" : "accept"}`);
}

And the Python equivalent, shaped as WSGI middleware so it sits in front of any WSGI app:

python
# WSGI-style guard: same idea, same number as nginx.
LIMIT = 20 * 1024 * 1024  # 20m

def body_too_large(environ, limit=LIMIT):
    raw = environ.get("CONTENT_LENGTH") or ""
    try:
        length = int(raw)
    except ValueError:
        return False  # no length: enforce while reading the stream
    return length > limit

def limit_body(app, limit=LIMIT):
    """Wrap a WSGI app so oversized bodies get a 413 before the app runs."""
    def wrapped(environ, start_response):
        if body_too_large(environ, limit):
            start_response("413 Request Entity Too Large", [("Content-Type", "text/plain")])
            return [b"413 Request Entity Too Large\n"]
        return app(environ, start_response)
    return wrapped

for raw in ["5242880", "20971520", "20971521", ""]:
    verdict = "413" if body_too_large({"CONTENT_LENGTH": raw}) else "accept"
    print(f"CONTENT_LENGTH={raw or '(none)':>9} -> {verdict}")

Both skip the check when there's no Content-Length, which is the chunked-upload case. There you have to count bytes as you read the stream. The nginx side doesn't need any of this; it already enforces its own limit before proxying. If you're setting up the proxy for the first time, the reverse proxy config example shows a complete server block to start from.

PHP and WordPress behind nginx#

PHP sites on nginx typically route .php requests through fastcgi_pass instead of proxy_pass, and the 413 works the same way: nginx rejects the body before PHP runs. Put client_max_body_size in the server block, or in the location that handles PHP. Since that is usually a regex location, a value set in location / won't reach it.

Docker and Kubernetes ingress#

In Docker the directive and the file are the same. What changes is where the file lives. Edit the configuration you mount into or bake into the container, then reload nginx inside the container or recreate it. Editing the file inside a running container without persisting the change means the next recreate brings the 413 back. The Docker Compose tutorial covers mounting config files into a service if you haven't done that before.

With a Kubernetes ingress controller, nginx.conf is generated for you. Editing it by hand is pointless because the controller rewrites it. Nginx-based controllers expose the body limit through an annotation on the Ingress object or a controller-wide setting, and that becomes client_max_body_size in the generated file. The exact annotation name depends on which controller you run, so take it from that controller's documentation rather than from a forum post.

One rule applies everywhere: if there's a load balancer, an ingress, and a sidecar nginx in front of your app, each one has its own limit, and the 413 comes from the first one that's too small. The Server header from curl -i is the quickest way to tell which hop replied.

Common mistakes that keep the 413 coming back#

  • Wrong file. The directive went into a config file nginx isn't loading. Check the path and any included files.
  • No reload, or a rolled-back reload. The master keeps the old config when the new one fails validation. Look in error.log.
  • Wrong server block. The value is in the port 80 block, but uploads go over HTTPS to a different block that still inherits the http value.
  • Wrong location. The value is in location /, but the upload URL matches a longer prefix or a regex location. Longest prefix and regex win.
  • Wrong directive. client_body_buffer_size was raised instead of client_max_body_size.
  • Another proxy in front. A second nginx, an ingress, or a load balancer rejects the body before your nginx sees it.
  • App limit lower than nginx's. The 413 now comes from Node, Python, or PHP. Match the numbers.
  • Reload as the wrong user. The signal has to come from the user that started nginx.

Quick reference#

Step Command or setting
Set the limit for one endpoint client_max_body_size 200m; inside that location
Set the limit for one site client_max_body_size 20m; inside that server
Apply the change nginx -s reload as the user that started nginx
Confirm the reload took Check error.log in /var/log/nginx or /usr/local/nginx/logs
Test under the limit curl -s -o /dev/null -w '%{http_code}\n' --data-binary @under.bin URL
Test over the limit Same command with a larger file, expect 413
Fix the next layer Set the app or PHP limit to the same value

Checklist:

  1. Find the config file nginx actually loads.
  2. Add client_max_body_size in the location or server that handles the upload.
  3. Reload, then read error.log.
  4. Upload one file under and one file over the limit with curl.
  5. Match the limit in the app behind nginx.
  6. If there's another proxy in front, repeat there.

Quick answers

What is the default client_max_body_size in nginx?

The 413 comes from the client_max_body_size directive, which you can raise in the nginx location or server that handles the upload.

Why does nginx still return 413 when my backend accepts large uploads?

Nginx checks the body size before it proxies the request, so the backend's limit never gets a say. Raise client_max_body_size in the nginx location or server that fronts the app, reload, and then make sure the app's own limit matches.

How do I fix 413 Request Entity Too Large on nginx ingress in Kubernetes?

Don't edit the generated nginx.conf; the controller overwrites it. Set the body size through the controller's Ingress annotation or global setting, using the name from that controller's documentation, and it becomes client_max_body_size in the generated config.

Does the fix work the same way for nginx in Docker?

Yes. The directive is identical; the difference is that the config file is mounted or baked into the container. Change that file, then reload nginx inside the container or recreate it, so the fix survives the next restart.

WordPress on nginx shows 413 on media upload. Which limit do I change?

The 413 is nginx's, so raise client_max_body_size in the server block or the location that handles PHP, then reload.

Did this solve it?

One tap. No account.

Saved you an hour? Send a tip.

USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.

Base

Network

0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
Open in wallet · 5 USDC

On a computer? Scan the code with your phone wallet, or copy the address.

Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.

Discussion

Start the discussion

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS