Troubleshooting

Header and body size limits at the proxy

Why requests die at the proxy before the app sees them: nginx buffer directives, 400/413/431/494/502, the upstream-sent-too-big-header fix, and a defaults table.

· 14 min read · How we verify this

Key points

  • Request headers, response headers and bodies are governed by three different sets of limits, and the error codes (400, 431, 494, 413, 502) tell you which one you hit.
  • upstream sent too big header while reading response header from upstream is a response header limit and produces a 502; raise proxy_buffer_size, and proxy_buffers with it.
  • nginx tries client_header_buffer_size (default 1k) first and only then allocates from large_client_header_buffers (default 4 8k), so the effective per-line limit is 8k, not 1k.
  • A 400 with no upstream log line and no application trace almost always means the request never reached the application.

When a request fails with 400, 413, 431, 494 or a 502 that leaves no trace in the application log, a size limit at the proxy is the first thing to check. Every proxy allocates fixed buffers for the request line, the request headers and the response headers, and rejects anything that does not fit before it ever contacts the upstream. The three limit families are independent and have different symptoms: request headers give you 400/431/494, response headers give you a 502 with a distinctive error-log line, and bodies give you 413.

The single most useful diagnostic signal is asymmetry between logs. If the proxy access log shows the request and the application log does not, the request was rejected at the proxy. If both show it but the client sees a 502, the response was rejected. That split narrows the search from a dozen directives to two or three.

Defaults you need to know#

LayerDirectiveDefaultApplies toError when exceeded
nginxclient_header_buffer_size1kfirst read of request line + headersnone directly (falls through to large buffers)
nginxlarge_client_header_buffers4 8kper header line, and total count400 to client and in the access log, via internal code 494; 414 if the request line is too long
nginxclient_max_body_size1mrequest body413
nginxclient_body_buffer_size8k or 16k (2 pages)body bufferingnone, spills to a temp file
nginxproxy_buffer_size4k or 8k (1 page)upstream response headers502, log: upstream sent too big header
nginxproxy_buffers8 4k or 8 8kupstream response body502 or disk spill
HAProxytune.bufsize16384whole request headers400
HAProxytune.maxrewrite1024reserved space inside the bufferreduces usable header space
HAProxytune.http.maxhdr101number of header fields400
Envoymax_request_headers_kb60total request headers431
Envoymax_headers_count100number of header fields431
Envoyper_connection_buffer_limit_bytes1048576per-connection buffering413 or stream reset
Node.js--max-http-header-size16384total request headers431, HPE_HEADER_OVERFLOW
Apache httpdLimitRequestFieldSize8190per header field400
Apache httpdLimitRequestFields100number of header fields400
Apache httpdLimitRequestLine8190request line414
Windows http.sys / IISMaxFieldLength16384per header field400 (logged by http.sys, not IIS)
AWS ALBrequest headers64 KB combinedtotal request headers400
AWS ALBrequest line16 KBmethod + URI + version400
AWS API Gatewaytotal header size (REST)20480 bytes (8000 for private APIs)request headers413

Two entries in that table are the ones people get wrong. tune.maxrewrite is subtracted from tune.bufsize, so HAProxy's usable request header space with defaults is 16384 minus 1024, roughly 15 KB, not 16 KB. And nginx's 4k/8k defaults that say "one page" really are page-size dependent: on a machine with 4 KB pages proxy_buffer_size is 4k, which is smaller than a single modern Set-Cookie burst.

How nginx's two request-header buffers interact#

This is the interaction that makes nginx's behaviour hard to predict from the docs alone. nginx first reads into a buffer of client_header_buffer_size (default 1k). If the request line and headers fit, nothing else is allocated. If they do not, nginx allocates a buffer from large_client_header_buffers (default 4 8k) and continues. So:

  • The request line must fit into one buffer. Too long and you get 414 with client sent too long URI in the error log.
  • Each individual header field must fit into one buffer. Too long and you get 400 with client sent too long header line: "Cookie: ..." in the error log. Both that line and client sent too long URI are written at info level, which the default error_log ... error; discards, so set error_log /var/log/nginx/error.log info; before looking for them.
  • The count limits how many large buffers exist at once, which bounds total header size at roughly 4 * 8k.

The practical consequence: raising client_header_buffer_size from 1k to 4k does not raise any limit. It only avoids a second allocation for typical requests, which is a small performance change, not a fix. To accept larger headers you must raise large_client_header_buffers:

nginx
http {
    client_header_buffer_size   4k;
    large_client_header_buffers 4 32k;
}

494 is nginx's internal, non-standard code for "request header too large". It is never sent on the wire, and by default it does not reach the access log either: ngx_http_special_response.c rewrites r->err_status to 400 for this code, so $status logs 400 like any other bad request. Its real use is as an error_page selector, and configuring error_page 494 ... is also what makes it visible in $status, because a matching error_page short-circuits the rewrite. If you write one, note that nginx must have parsed enough of the request to route it, which is not guaranteed.

The most-searched error in this family#

text
2026/09/08 11:04:22 [error] 1234#0: *5678 upstream sent too big header while
reading response header from upstream, client: 10.0.0.5, server: app.example.com,
request: "GET /login HTTP/1.1", upstream: "http://10.0.1.10:8080/login",
host: "app.example.com"

The client sees a 502 Bad Gateway. Nothing is wrong with the upstream: it returned a perfectly valid response whose header block did not fit into proxy_buffer_size. The usual trigger is a login endpoint that sets several Set-Cookie headers, or an identity proxy that reflects a session token back.

The fix that actually works:

nginx
location / {
    proxy_pass http://app_backend;

    proxy_buffer_size       32k;   # must hold the entire response header block
    proxy_buffers           8 32k; # each buffer at least as large
    proxy_busy_buffers_size 64k;
}

Also note that proxy_buffer_size bounds the header block even when proxy_buffering off is set. Turning buffering off is a common (and wrong) first response to this error, because it does nothing for headers. See proxy buffering and streaming responses for what buffering does control, and 502 vs 503 vs 504 for the other causes of the same status code.

Body limits#

nginx's client_max_body_size defaults to 1m and rejects larger bodies with 413 Request Entity Too Large plus client intended to send too large body: 5242880 bytes in the error log. 0 disables the check.

There is a client-visible wrinkle. nginx can decide to reject on the Content-Length header before reading the body, then close the connection while the client is still uploading. Browsers frequently surface that as a network error or a reset rather than the 413, so the user reports "the upload just fails" and the 413 is only visible in the proxy log. client_body_timeout and lingering close settings affect how often this happens, but the reliable diagnosis is the access log.

HAProxy is different in kind: it does not buffer whole request bodies, so there is no client_max_body_size equivalent. You can only reject on the declared length or on what fits in a buffer:

haproxy
frontend fe_main
    option http-buffer-request
    http-request deny deny_status 413 if { req.hdr(content-length),int gt 10485760 }

option http-buffer-request waits for the body, but only up to tune.bufsize, so it is a guard for small payloads, not a general upload limit. Enforce large-upload limits at the application or at an nginx tier. See HAProxy configuration for where these belong in the frontend.

Envoy enforces bodies with the buffer filter's max_request_bytes, returning 413. AWS ALB does not impose a general request body limit for HTTP targets, but a Lambda target caps the payload at 1 MB, and API Gateway REST APIs cap payloads at 10 MB, which is a hard service quota that no configuration raises.

Where the bytes actually come from#

Header bloat is rarely one big header. It is accumulation, and the sources are predictable:

  • Cookies. RFC 6265 asks user agents to support at least 4096 bytes per cookie and at least 50 cookies per domain. A site with an analytics cookie, a consent cookie, a session cookie and four feature flags easily crosses 8 KB, and every one of them is sent on every request to that domain, including images.
  • JWTs in Authorization. A token carrying group memberships or fine-grained scopes grows with the user's permissions, so the request works for a normal user and fails for an administrator. That is the signature of this class of bug: the size depends on who is logged in.
  • SAML assertions and identity headers. Authenticating proxies commonly inject X-Forwarded-User, X-Forwarded-Groups, or a base64 assertion. A user in 300 AD groups produces a header that no default buffer accommodates. The same effect appears with Kerberos and NTLM tokens on Windows, which is why http.sys exposes MaxFieldLength; see proxy authentication.
  • Long Referer. Single-page applications with state encoded in the query string produce referers of several kilobytes.
  • Redirect loops that accumulate cookies. Each hop of an auth bounce sets another cookie or another state parameter; by the fourth redirect the header block has doubled and the request that would have succeeded first time now returns 400. The visible symptom is a redirect loop that terminates in a 400 rather than in the browser's loop detection.
  • X-Forwarded-For in a long chain. Each hop appends an address. It is small, but it is not zero, and it is the thing that pushed the request over the edge in production and not in staging.
  • Kubernetes ingress defaults. ingress-nginx sets proxy-body-size to 1m by default. proxy-buffer-size is available as a per-Ingress annotation, but large-client-header-buffers is a ConfigMap setting only, so request header limits cannot be raised for a single Ingress. That asymmetry catches teams who fix the response side per-service and then cannot fix the request side the same way.

Diagnosis procedure#

1. Measure the real header size. curl reports the bytes it sent:

bash
curl -s -o /dev/null \
     -w 'request_bytes=%{size_request} response_header_bytes=%{size_header} status=%{http_code}\n' \
     -b "$(cat real_cookies.txt)" https://app.example.com/login

In a browser, document.cookie.length gives the cookie contribution, and the network panel's request-headers view gives the rest. On the server side, nginx's $request_length variable logs the total request size including headers and body, so adding it to log_format turns this into a standing measurement rather than a one-off.

2. Reproduce with a synthetic header and find the exact threshold by bisection:

bash
for n in 1000 4000 8000 16000 32000; do
  printf '%s ' "$n"
  curl -s -o /dev/null -w '%{http_code}\n' \
    -H "X-Pad: $(head -c $n /dev/zero | tr '\0' 'a')" \
    https://app.example.com/healthz
done

Output like 1000 200 / 4000 200 / 8000 400 / 16000 400 places the limit between 4 KB and 8 KB, which matches Apache's LimitRequestFieldSize 8190 or a proxy at defaults. A 431 instead of 400 points at Node.js or Envoy. A 400 paired with client sent too long header line in an info-level nginx error log points at nginx.

3. Bisect the chain. Run the same loop directly against the upstream, bypassing each proxy in turn:

bash
curl -s -o /dev/null -w '%{http_code}\n' \
  --resolve app.example.com:443:10.0.1.10 \
  -H "X-Pad: $(head -c 16000 /dev/zero | tr '\0' 'a')" \
  https://app.example.com/healthz

The innermost hop that still rejects the request owns the limit. The full method is in the proxy debugging playbook.

4. For response headers, look at the upstream directly. curl -sD - -o /dev/null http://10.0.1.10:8080/login | wc -c gives the response header block size. Compare it with proxy_buffer_size.

Failure modes#

SymptomLayerRoot causeFix
400 in browser, client sent too long header line in the nginx error logrequest headersone header line above 8kraise large_client_header_buffers
414 Request-URI Too Largerequest linelong query stringraise large_client_header_buffers, or move state to a POST body
431 Request Header Fields Too Largerequest headersNode or Envoy limit--max-http-header-size, or max_request_headers_kb
502 plus upstream sent too big headerresponse headersupstream Set-Cookie burstraise proxy_buffer_size and proxy_buffers
413 on uploadbodyclient_max_body_size 1mraise it at every tier, including the ingress ConfigMap
Works for one user, fails for anotherrequest headerspermission-dependent token or group headermeasure the header for the failing user
Works over HTTP/1.1, fails over HTTP/2request headersHPACK-decoded size counted against the limitraise the limit; compression on the wire does not reduce the decoded size
Fails only after several redirectsrequest headerscookie accumulation per hopfix the redirect loop, not the buffer

The HTTP/2 row deserves emphasis. HPACK compresses headers on the wire, but proxies enforce limits against the decoded size, so a request that measures 2 KB in a packet capture can still be rejected for exceeding a 16 KB limit. Do not use wire byte counts as evidence when debugging an HTTP/2 or HTTP/3 hop; see HTTP/2 and HTTP/3 through proxies.

Choosing the numbers#

Do not set every limit to a very large value. Header buffers are allocated per connection while the request is being read, so large_client_header_buffers 4 256k allows a single client to make the proxy allocate a megabyte per connection, which is a cheap denial-of-service. The defaults exist because they are comfortably above legitimate traffic.

A workable rule: measure the 99th-percentile $request_length from your own access logs, round up to the next power of two, and add one step of headroom. That usually lands on 16k or 32k for header buffers on an authenticated application, and it gives you a number you can defend, rather than one copied from an answer on the internet. Then fix the cause: move group membership out of a header and into a token reference, scope cookies to the paths that need them, and set Domain narrowly so that static asset requests do not carry session state.

Frequently asked questions#

What does "upstream sent too big header while reading response header from upstream" mean?#

It means the upstream's response header block was larger than nginx's proxy_buffer_size, so nginx discarded the response and returned 502 Bad Gateway to the client. The upstream is healthy. Raise proxy_buffer_size (for example to 32k) and raise proxy_buffers to match, because nginx enforces a relationship between those and proxy_busy_buffers_size.

What is nginx status 494?#

494 is nginx's non-standard internal code for "request header too large". It is never sent to the client, which receives 400 Bad Request, and it does not appear in the access log by default either, because nginx rewrites the status to 400 before generating the response. It exists so that you can write error_page 494 ...; adding that directive is also what makes 494 show up in $status. Without it, the conclusive evidence is the info-level error-log line client sent too long header line.

Why do I get a 400 with no matching entry in my application log?#

Because the proxy rejected the request before proxying it. A size limit, a malformed header or an invalid request line all produce this. Compare the proxy access log with the application log for the same timestamp; if only the proxy has the request, the limit is at the proxy.

Does raising client_header_buffer_size fix "request header too large"?#

No. nginx tries that buffer first (default 1k) and, when the headers do not fit, allocates a larger one from large_client_header_buffers (default 4 8k). The effective limit is the large buffer size, so that is the directive to change.

What is the maximum HTTP header size?#

There is no limit in the HTTP specification; it is entirely implementation defined. In practice the binding constraint in a typical chain is between 8 KB and 64 KB: Apache allows 8190 bytes per field, nginx 8 KB per line, Node.js 16 KB total, HAProxy roughly 15 KB total at defaults, AWS ALB 64 KB total and Envoy 60 KB total. Design for the smallest hop in your chain.

Why does a request work for some users and fail for others?#

Almost always because the header size depends on the user: a JWT with more scopes, more group memberships in an identity header, or more cookies from a longer session. Capture the actual request for a failing user and measure it with curl -w '%{size_request}' or $request_length.

How do I raise the body size limit on a Kubernetes ingress?#

Set the nginx.ingress.kubernetes.io/proxy-body-size annotation on the Ingress (for example 50m), or the proxy-body-size key in the controller ConfigMap for a cluster-wide default. Remember that any load balancer or WAF in front of the ingress controller has its own limit that the annotation does not affect.

Does HTTP/2 header compression let me send bigger headers?#

No. HPACK reduces the bytes on the wire, but proxies apply their limits to the decoded header block, and Envoy's max_request_headers_kb and Node's --max-http-header-size both count uncompressed size. A capture showing small frames is not evidence that the headers are small.

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.

  1. nginx ngx_http_core_module (client_header_buffer_size, large_client_header_buffers, client_max_body_size)
  2. nginx ngx_http_proxy_module (proxy_buffer_size, proxy_buffers, proxy_busy_buffers_size)
  3. HAProxy configuration manual (tune.bufsize, tune.maxrewrite, tune.http.maxhdr)
  4. Envoy HTTP connection manager protocol options
  5. Node.js CLI, --max-http-header-size
  6. Apache httpd core directives (LimitRequestFieldSize, LimitRequestFields)
  7. RFC 9110 HTTP Semantics, status codes 400 and 413
  8. RFC 6585, status code 431 Request Header Fields Too Large
  9. RFC 6265 HTTP State Management Mechanism, cookie size limits
  10. AWS Application Load Balancer quotas

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#