How HTTP Works

Debug guide · you're seeing

  • 400 Bad Request: Request Header Or Cookie Too Large
  • 431 Request Header Fields Too Large
  • HTTP ERROR 431
  • Your browser sent a request that this server could not understand. Size of a request header field exceeds server limit.
  • client sent too long header line

Request Header Or Cookie Too Large: Fix 400 and 431

Fix 400 Request Header Or Cookie Too Large and 431 errors: find the oversized cookie or token, then check nginx, Apache, Node, ALB and Cloudflare limits.

Reviewed 9 min readintermediate8 sourcesTry itMarkdown
On this page

TL;DR: The request’s headers, almost always the Cookie line or an Authorization token, exceeded a server limit. nginx answers 400 Request Header Or Cookie Too Large when one header line exceeds an 8 KB buffer (large_client_header_buffers 4 8k), Apache answers 400 above 8,190 bytes per field, and Node answers 431 above 16 KiB in total. Clearing the site’s cookies fixes it for one user; for everyone, find the cookie that grew, stop it growing, and only then raise limits.

What it means

The client sent more header bytes than some server in the path will buffer. RFC 6585 defines 431 Request Header Fields Too Large for this, and allows it both when the total is too large and when a single field is. In practice most servers still answer 400, so the status code alone does not tell you much. The body and the limit do:

LayerLimit (default)Status and what the client sees
nginxOne header line must fit in one 8 KB buffer; the whole header in 4 of them (large_client_header_buffers 4 8k)400, page title 400 Request Header Or Cookie Too Large
Apache httpd8,190 bytes per header field (LimitRequestFieldSize), 100 fields (LimitRequestFields)400, “Size of a request header field exceeds server limit.”
Node.js (http, Express, Next.js, Vite dev servers)16 KiB for all headers together (--max-http-header-size)431 with an empty body; Chrome shows HTTP ERROR 431
AWS Application Load Balancer16 KB request line, 16 KB per header, 64 KB in total; not adjustable400
Cloudflare128 KB of request headers in total, 16 KB URLAn error page served by Cloudflare itself (check for a Cloudflare error page, not your origin’s)

The nginx response, which reaches the browser even when Cloudflare or another CDN sits in front:

HTTP/1.1 400 Bad Request
Server: nginx
Content-Type: text/html
Connection: close

<html>
<head><title>400 Request Header Or Cookie Too Large</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>Request Header Or Cookie Too Large</center>

The Node response is just two lines, which is why the browser shows its own error page instead of a server page:

HTTP/1.1 431 Request Header Fields Too Large
Connection: close

Who sent it?

  • Page title 400 Request Header Or Cookie Too Large and Server: nginx: nginx, either your own or an ingress controller. A Cloudflare CF-Ray header on the same response only means Cloudflare relayed it; with a 128 KB allowance, Cloudflare is rarely the layer that refused.
  • “Size of a request header field exceeds server limit.” in an Apache-styled page: Apache httpd.
  • Empty body, 431, “HTTP ERROR 431” in Chrome: a Node.js server, very often a local dev server.
  • Server: awselb/2.0 with a 400: the ALB rejected it before your targets saw anything. Its limits cannot be raised, so the only fix is smaller headers.

The logs are quiet by default. nginx records client sent too long header line: "Cookie: ..." at the info level, and Apache records AH00561: Request header exceeds LimitRequestFieldSize: Cookie at info too. With the usual error_log ... error; and LogLevel warn, nothing appears. Lower the level temporarily to see which header is at fault:

error_log /var/log/nginx/error.log info;

Why headers get that big

  • Cookies on a parent domain. A cookie set with Domain=example.com is sent to www.example.com, app.example.com, api.example.com and every other subdomain. Each team’s analytics, A/B testing, consent and feature-flag cookies add up on every host, and nobody owns the total. Scope cookies to the host that needs them by leaving out Domain.
  • State stored in cookies. A JWT or serialized session with user roles, group memberships or a cart grows with the user. A cookie can legally hold about 4 KB of name and value, and RFC 6265bis asks browsers to keep at least 50 cookies per domain, so the browser will happily store far more than an 8 KB nginx buffer can take.
  • Large Authorization headers. Bearer tokens that embed group claims grow with the user’s group count. Apache’s own documentation notes that SPNEGO (Kerberos) headers can reach 12,392 bytes, already above its 8,190-byte default.
  • Long URLs and Referer. A long query string makes the request line and the Referer of every subresource request long. Same-origin requests send the full URL as Referer under the default strict-origin-when-cross-origin policy. In nginx an over-long request line returns 414 URI Too Long, not 400.
  • localhost. Cookies do not provide isolation by port (RFC 6265 Section 8.5), so every project you have run on localhost shares one cookie jar.

Measure it

In the DevTools console on the affected site:

// Total bytes of cookies visible to JavaScript on this page
new Blob([document.cookie]).size

// The ten largest cookies visible to JavaScript
document.cookie.split('; ')
  .map((c) => [c.split('=')[0], c.length])
  .sort((a, b) => b[1] - a[1])
  .slice(0, 10)

document.cookie does not include HttpOnly cookies, which are usually the session and auth cookies, so treat the number as a minimum. For the full list open DevTools, Application, Cookies: the Size column shows each cookie’s size in bytes, including HttpOnly ones. The Network panel shows the exact Cookie line the browser sent with the failing request.

From the command line, curl -v prints the request headers with >. Paste the browser’s Cookie value into a file and count what a request actually carries:

curl -sv -o /dev/null -H "Cookie: $(cat cookies.txt)" https://example.com/ 2>&1 \
  | grep '^> ' | wc -c

To reproduce the error without a browser, send a single 9,000-byte header:

curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Cookie: big=$(head -c 9000 /dev/zero | tr '\0' a)" https://example.com/

nginx with default settings returns 400. Splitting the value into several cookies does not help: over HTTP/1.1 the browser sends all cookies on one Cookie line, so their combined size has to fit in a single 8 KB buffer, not in the 32 KB that four buffers add up to.

Fix it, in order of likelihood

  1. Unblock the user: delete the site’s cookies, including those on the parent domain, and reload. Support staff can usually give this instruction faster than you can deploy anything.
  2. Find the offender with the DevTools Size column or the info-level log line, which names the header.
  3. Stop the growth: remove cookies nobody reads, set short Max-Age on marketing and experiment cookies, scope cookies to a single host, and replace a cookie-borne JWT or session blob with an opaque session ID (see below).
  4. Raise limits only where needed and in matching order, so the first layer does not accept something the next one rejects.

nginx

large_client_header_buffers is valid only in http and server blocks, not in location. When it is set in a server block, nginx may use the value from the default server for that address and port, because the virtual host is picked from the Host header after the header has been read. Setting it in http avoids that trap.

http {
    # Default: 4 8k. One header line must fit in one buffer.
    large_client_header_buffers 4 16k;
}

Reload with nginx -t && nginx -s reload. The same limits apply to HTTP/2 and HTTP/3, where a buffer limits each header field after HPACK or QPACK compression. Check nginx -T | grep large_client_header_buffers to confirm which value is loaded.

If nginx proxies to Node, raising nginx to 16k per line does not help a request whose total exceeds Node’s 16 KiB; Node then answers 431 behind nginx.

Kubernetes ingress-nginx

The controller ConfigMap key is large-client-header-buffers, default "4 8k":

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  large-client-header-buffers: "4 16k"

Apache httpd

# Defaults: 8190 bytes per field, 100 fields. Server config or virtual host only.
LimitRequestFieldSize 16380
LimitRequestLine      16380

With name-based virtual hosts the value from the first-listed virtual host for the address and port is used, so set it there or globally.

Node.js, Express and dev servers

# CLI flag
node --max-http-header-size=32768 server.js

# Works for tools that start Node for you (next dev, vite, nodemon)
NODE_OPTIONS=--max-http-header-size=32768 npm run dev
import http from 'node:http'

// Per-server override of the 16 KiB default
const server = http.createServer({ maxHeaderSize: 32768 }, app)
server.listen(3000)

For the localhost case, clearing cookies for localhost is the real fix.

Response headers: the other direction

A large Set-Cookie coming back from your app can break nginx too, with a different symptom: upstream sent too big header while reading response header from upstream in the error log and a 502 Bad Gateway. That is controlled by proxy_buffer_size (one memory page, 4k or 8k by default), not by large_client_header_buffers.

Why raising limits is a stopgap

Raising a limit hides the growth rather than stopping it. Every byte in a parent-domain cookie is sent with every request to every subdomain, including requests for images, scripts and API calls. A user who adds groups or keeps a long-lived session keeps adding bytes until they hit the next limit, and some limits cannot be raised at all: the ALB’s 16 KB per header is fixed. Every proxy, WAF and API gateway on the path also has its own ceiling, and you may not control all of them.

The durable fix is to keep state on the server:

HTTP/1.1 200 OK
Set-Cookie: __Host-sid=8f3a1c9e2b7d4f60; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400

The cookie holds a random ID, and the session data (roles, cart, preferences) lives in Redis or the database, keyed by that ID. The __Host- prefix forbids a Domain attribute, so the cookie cannot spread to sibling subdomains. For APIs that need a self-contained token, keep the claims minimal and look permissions up server-side instead of embedding every group in the token.

To clear bloated cookies for users who are already locked out, serve a response with Clear-Site-Data: "cookies" from a host that can still accept their requests. MDN describes its scope as the registered domain, including subdomains.

Reproduce and verify

# Before the fix: 400 from nginx, 431 from Node
curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Cookie: big=$(head -c 9000 /dev/zero | tr '\0' a)" https://example.com/

# Straight to the origin, skipping the CDN or load balancer
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: example.com' \
  -H "Cookie: big=$(head -c 9000 /dev/zero | tr '\0' a)" http://ORIGIN_IP/

If the origin accepts the request and the public URL rejects it, the limit is in front of the origin. After shrinking the cookies, the DevTools Size column total for the site should sit comfortably below the smallest limit in your path.

Frequently asked questions

How do I fix "400 Bad Request: Request Header Or Cookie Too Large"?

As a visitor, delete the cookies for that site, including the parent domain, and reload. As the site owner, find the oversized cookie or Authorization header and stop it growing. Raising nginx large_client_header_buffers (default 4 8k) in the http block makes the error go away but only until the next limit downstream.

Why does nginx return 400 instead of 431 for large headers?

nginx handles oversized headers with an internal code (494) and sends it to the client as 400 Bad Request with the page title "400 Request Header Or Cookie Too Large". Apache and AWS Application Load Balancers also answer 400. Node.js is the common server that answers 431 Request Header Fields Too Large.

What is the maximum header size in Node.js?

The default is 16 KiB (16,384 bytes) for the whole header block since Node 13.13.0; it was 8 KiB before. Change it with the --max-http-header-size flag, the NODE_OPTIONS environment variable, or the maxHeaderSize option of http.createServer. When the limit is exceeded Node replies 431 with an empty body, which Chrome shows as "HTTP ERROR 431".

Why do I get 431 on localhost but not in production?

Cookies are not isolated by port. Every app you have run on localhost:3000, :5173, :8080 and so on writes into the same cookie jar for "localhost", and all of them are sent to whichever dev server you open. Clear cookies for localhost, or use distinct hostnames such as app.localhost for each project.

Does document.cookie.length show the real Cookie header size?

It undercounts. document.cookie omits HttpOnly cookies, which are often the largest ones (sessions and auth tokens), and it only shows cookies for the current path. Use the Size column in DevTools Application > Cookies, or the Cookie line of the failing request in the Network panel, for the full picture.

Should I just raise the header size limit?

Only as a stopgap. Large cookies are sent on every request to every matching host, including images and scripts, and every proxy in the path has its own limit; an AWS ALB, for example, caps a single header at 16 KB and cannot be changed. Store session data server-side and keep only an opaque ID in the cookie.

Sources

  1. nginx: ngx_http_core_module large_client_header_buffersnginx.org
  2. Apache HTTP Server: LimitRequestFieldSizehttpd.apache.org
  3. Node.js CLI: --max-http-header-sizenodejs.org
  4. MDN: 431 Request Header Fields Too Largedeveloper.mozilla.org
  5. RFC 6585 Section 5: 431 Request Header Fields Too Largerfc-editor.org
  6. RFC 6265 Section 8.5: Cookies do not provide isolation by portrfc-editor.org
  7. AWS Docs: Quotas for your Application Load Balancers (HTTP headers)docs.aws.amazon.com
  8. Cloudflare Docs: Connection limits (request limits)developers.cloudflare.com

Keep going

Browse /search