NGINX proxy buffer tuning: proxy_buffers, proxy_buffer_size, and busy buffers

When proxy_buffering is on (the default), NGINX absorbs the upstream response in memory before sending it to the client. This shields backends from slow clients and enables compression, but three directives control it: proxy_buffer_size for headers, proxy_buffers for the body, and proxy_busy_buffers_size for the in-flight flush window. Misconfiguration causes 502s, silent disk spills, and reload failures.

The defaults are modest: eight body buffers of one memory page each, and one header page (typically 4K or 8K). That works for static sites and small JSON, but it fails for modern workloads: APIs with large JWT tokens in headers, bulk exports returning multi-megabyte JSON, and Server-Sent Events streams. Undersized body buffers spill to disk. Undersized header buffers return 502. Invalid proxy_busy_buffers_size values prevent NGINX from starting or reloading.

What it is and why it matters

proxy_buffer_size sets the buffer for the upstream response header. The default is one memory page. If headers exceed this value, NGINX logs “upstream sent too big header while reading response header” and returns 502. API workloads with large Set-Cookie headers, Content-Security-Policy directives, or traced request metadata often need more.

proxy_buffers sets the number and size of body buffers per connection. The default is 8 buffers of one memory page. These are allocated per connection in each worker. If the full response fits, NGINX buffers it in memory and flushes to the client at its own pace. If the response exceeds the total size, NGINX writes the remainder to temporary files under proxy_temp_path.

proxy_busy_buffers_size controls how much data can be in the “busy” state: buffers actively being sent to the client while the upstream response is still arriving. Its default is twice the size of either proxy_buffer_size or a single proxy_buffers buffer, whichever is larger. It must be at least as large as the bigger of those two, and no larger than the total proxy_buffers size minus one buffer. Violating this range causes nginx -t to fail with an [emerg] error and the configuration will not load.

proxy_buffering enables or disables the entire mechanism. The default is on. When on, NGINX fills proxy buffers before sending to the client. When off, the response streams synchronously as received.

How it works

Per response, NGINX reads the upstream status line and headers into the single proxy_buffer_size buffer. If they do not fit, the request fails immediately with 502. There is no fallback.

If the headers fit, NGINX reads the body into the cyclic proxy_buffers pool. As each buffer fills, it can be marked busy and flushed to the client, provided the total busy volume does not exceed proxy_busy_buffers_size. This is the high-water mark for data allowed in flight to the client while the upstream is still transmitting.

If the upstream sends data faster than the client receives it, the buffers fill up. Once all proxy_buffers are full and the response is not yet complete, NGINX writes chunks to temporary files on disk under proxy_temp_path. The response is then served from a mix of memory buffers and disk files. The spill is silent: there is no error log entry. Evidence is elevated latency and disk I/O on the temporary file partition. Because the worker writes to disk while still reading from the upstream, a slow partition adds back-pressure to the upstream connection.

When proxy_buffering is off, none of this happens. NGINX streams the response to the client synchronously as it arrives. This is required for Server-Sent Events and long-lived streams, because buffering would stall the connection indefinitely. The trade-off is that the upstream must wait for the client to receive each chunk, and NGINX cannot efficiently apply compression or retry on timeout. With buffering off, $upstream_response_time includes time spent sending to the client, not just time waiting for the upstream.

flowchart LR
    U[Upstream response] --> H{Headers}
    H -->|fit| HS[proxy_buffer_size]
    H -->|exceeds| ERR[502 invalid header]
    HS --> B{Body}
    B -->|fill| PB[proxy_buffers]
    PB -->|flush window| PBB[proxy_busy_buffers_size]
    PBB --> C[Client]
    PB -->|exceeds capacity| DISK[proxy_temp_path]
    DISK --> C
    U -.->|proxy_buffering off| C

Where it shows up in production

The most common symptom of undersized body buffers is silent disk I/O on the proxy_temp_path partition. NGINX does not log a warning when it spills to disk. The signals are elevated $request_time values that far exceed $upstream_response_time, plus disk latency on the temporary file partition. If your application returns large JSON blobs or CSV exports and you have not tuned proxy_buffers, you are likely serving responses from disk. Use iostat -x 1 on that partition during peak load to confirm disk-bound spills.

Undersized header buffers appear as intermittent 502s that correlate with specific endpoints, not upstream downtime. APIs that inject large trace headers or authentication cookies can exceed the default header buffer and trigger the “upstream sent too big header while reading response header” error. Raising proxy_buffer_size to 16K or 32K usually resolves this.

Streaming endpoints fail when proxy_buffering is left on for Server-Sent Events or long-lived HTTP stream locations. The client receives nothing until the upstream closes the connection, which for an event stream may be never. The connection hangs. The fix is disabling buffering for that location, not adjusting buffer sizes. Compression also interferes with streaming: gzip or brotli forces NGINX to accumulate chunks before compressing, which stalls the stream.

Some ingress-nginx releases introduced hardcoded defaults for proxy_busy_buffers_size that conflicted with user-defined proxy-buffer-size values, causing nginx -t to fail with the message that proxy_busy_buffers_size must be less than the size of all proxy_buffers minus one buffer. If you run ingress-nginx, verify your controller version and check that buffer directives align.

Tradeoffs and when to use it

Larger proxy_buffers keep data in RAM and eliminate disk I/O for large responses, but each worker allocates that memory per connection. Sixteen buffers of 256K consume up to 4MB per connection. A worker handling one hundred concurrent proxy connections could allocate 400MB just for proxy buffers. Under high concurrency, this can exhaust memory or push workers toward OOM. Size buffers for your P99 response body, not the maximum theoretical payload.

Total proxy buffer memory per worker roughly equals (proxy_buffers count x size) + proxy_buffer_size + proxy_busy_buffers_size. Calculate this upper bound before raising values in high-concurrency environments.

For API-heavy workloads, raise proxy_buffer_size independently of proxy_buffers. Headers and bodies scale differently. A 16K header buffer with the default 8 x 8K body buffer is often the right starting point for modern APIs.

For Server-Sent Events and long-lived HTTP streams, disable buffering entirely:

location /events {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_read_timeout 86400s;
}

For WebSocket proxying, also add proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection 'upgrade';.

Compression must also be disabled for these locations, because gzip forces NGINX to accumulate chunks before compressing, defeating the stream.

For proxy_busy_buffers_size, the safest approach is to leave it at the default unless you have a specific reason to change it. If you do change it, remember the enforced constraint. For example, if proxy_buffers is set to 16 256k, the total buffer space is 4MB. Valid proxy_busy_buffers_size must then be at least 256K and at most (16 - 1) * 256k, or 3.75MB. Setting it to 4MB would cause a config test failure. This constraint ensures that at least one full buffer remains available for reading from the upstream while others are busy flushing.

Signals to watch in production

SignalWhy it mattersWarning sign
502 rate with upstream header errorsproxy_buffer_size too small for response headersError log: “upstream sent too big header while reading response header”
$request_time minus $upstream_response_timeLarge gap indicates NGINX overhead, client slowness, or disk flush from buffer spillGap grows on large responses while upstream time stays flat
Disk I/O on proxy_temp_path partitionBuffer overflow writes bodies to diskLatency spikes correlating with large response sizes
Worker RSS memoryEach connection allocates proxy_buffers in fullRSS per worker exceeds baseline for current connection count
Active connections in Writing stateIncludes time flushing buffers to clientWriting dominates with normal upstream latency, indicating output-side delay

How Netdata helps

  • Netdata correlates $request_time and $upstream_response_time from access logs, surfacing the gap that reveals disk spilling or slow client flushes.
  • Disk latency and utilization charts for the proxy_temp_path partition catch silent buffer overflows.
  • Per-worker RSS memory tracking alerts when buffer bloat causes memory growth beyond what connection count explains.
  • Error log monitoring detects “upstream sent too big header” and [emerg] config test failures.
  • The active connection state breakdown (Reading / Writing / Waiting) isolates whether high Writing counts stem from upstream delays or from NGINX flushing to slow clients.