Fixing Silent Failures When Nginx Truncates Upstream Responses
Your API returns a perfectly valid JSON response from your backend server, but the client keeps getting a malformed or cut-off body. No 502, no 504, no error in your application logs β just a broken response that appears fine until someone actually reads it. This is one of the most frustrating classes of Nginx bugs because the failure is invisible by default.
The root cause is almost always Nginx's proxying and buffering layer quietly doing something you didn't intend. Once you know where to look, the fix takes minutes.
What you'll learn
- Why Nginx truncates upstream responses without logging an error
- How proxy buffering works and where it breaks down
- How to read the right Nginx debug signals
- The specific directives to tune for large or streaming responses
- How to verify your fix is actually working
Prerequisites
You should be comfortable editing Nginx configuration files and reloading the service. The examples here use a typical reverse-proxy setup where Nginx sits in front of a backend application (Node.js, Python, Java β it doesn't matter). You'll need access to Nginx error logs, ideally with debug or warn log level temporarily enabled.
Why Nginx Truncates Responses Silently
Nginx's proxy module buffers upstream responses in memory before forwarding them to the client. When a response fits inside the allocated buffers, everything is fine. When it doesn't, Nginx either spills to disk or closes the upstream connection β and depending on your configuration, it may close that connection before the client has received the full body.
The critical detail: this is not treated as an error by Nginx's default logging configuration. Nginx considers the job done once it starts forwarding. If it decides to stop buffering partway through, it moves on without writing anything to the error log at the default error level. That silence is what makes this so hard to catch.
There are three distinct scenarios that cause truncation:
- Buffer overflow with disk spill disabled or failing β the response is larger than the in-memory buffers, Nginx tries to write to a temp file, and the temp file path is not writable or disk space is exhausted.
- Upstream read timeout β the backend takes too long to send all the data, Nginx closes the upstream connection mid-stream.
- Upstream sends a premature close β less common, but your backend or a load balancer upstream closes the TCP connection before the final bytes arrive.
Reading the Logs You Actually Need
Before touching any config, get better visibility. Edit your Nginx log format to capture upstream response details. In your http or server block, add or modify your log format:
log_format detailed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'ups_status=$upstream_status '
'ups_bytes=$upstream_response_length '
'ups_time=$upstream_response_time';Apply this format to your access log with access_log /var/log/nginx/access.log detailed;. Now each request line shows you the upstream HTTP status, the number of bytes the upstream sent, and how long the upstream took to respond. Compare ups_bytes to the Content-Length header your application sends. If they differ, you've confirmed truncation.
For deeper diagnostics, temporarily set your error log level to warn or even debug:
error_log /var/log/nginx/error.log warn;Reload with nginx -s reload and reproduce the problem. Look for lines containing upstream prematurely closed connection or no live upstreams while connecting to upstream. These give you a concrete starting point.
The Buffer Configuration Explained
Nginx's proxy buffering is controlled by a small set of directives. Understanding what each one does prevents you from tuning blindly.
| Directive | What it controls | Default |
|---|---|---|
proxy_buffering | Whether buffering is on at all | on |
proxy_buffer_size | Size of the buffer for the first part of the response (headers) | 4k or 8k |
proxy_buffers | Number and size of buffers for the response body | 8 4k or 8 8k |
proxy_busy_buffers_size | Max buffer space that can be sending to client while upstream is still being read | 8k or 16k |
proxy_temp_path | Directory for temp files when buffers fill up | system default |
proxy_max_temp_file_size | Max size of temp file (0 disables temp files) | 1024m |
The most common mistake is leaving all of these at defaults while your upstream starts sending responses that are larger than the combined buffer space. When the buffers are full and the temp file path isn't writable, Nginx quietly drops the connection.
Fixing Buffer-Related Truncation
For a typical API that returns responses up to a few megabytes, a reasonable starting configuration looks like this:
location /api/ {
proxy_pass http://backend;
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;
proxy_temp_path /var/cache/nginx/proxy_temp;
proxy_max_temp_file_size 256m;
}The values here are not universal β they depend on your typical response size. A good rule of thumb: set proxy_buffer_size large enough to hold the response headers plus the first chunk of body (headers are rarely more than 8β16k). Set proxy_buffers so the total buffer space (number Γ size) comfortably holds your median response. For a service that returns 200k JSON objects, eight 32k buffers gives you 256k total β enough headroom.
Make sure the proxy_temp_path directory exists and is owned by the Nginx worker user (usually www-data or nginx):
mkdir -p /var/cache/nginx/proxy_temp
chown nginx:nginx /var/cache/nginx/proxy_tempIf you're dealing with very large responses (file downloads, exports, reports), consider turning off buffering entirely for those locations. This tells Nginx to pass bytes from upstream to client as they arrive, which removes the buffer size problem entirely:
location /exports/ {
proxy_pass http://backend;
proxy_buffering off;
}The trade-off is that disabling buffering holds the upstream connection open until the client finishes downloading, which can exhaust upstream worker connections under heavy load. Use it selectively.
Fixing Timeout-Related Truncation
If your ups_time values in the access log are close to 60 seconds and responses are getting cut off, you're hitting the default proxy_read_timeout. This timeout governs how long Nginx waits between successive reads from the upstream β not the total response time. If your backend pauses between chunks of a large streamed response, a 60-second gap will close the connection.
location /api/ {
proxy_pass http://backend;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 120s;
}Set proxy_read_timeout to match the realistic worst-case pause between upstream writes. For a backend that streams chunks every 30 seconds, 90β120 seconds gives you a sensible safety margin. Don't simply set it to a huge number (like 3600s) everywhere β you'll mask genuinely stalled connections and waste resources.
Diagnosing Upstream Premature Close
When the error log says upstream prematurely closed connection while reading response header from upstream, the backend is closing the TCP connection before sending a complete response. This is distinct from Nginx truncating things β the upstream is at fault here.
Common causes include:
- Gunicorn or uWSGI worker timeout killing a slow request mid-flight
- A Node.js uncaught exception closing the socket before the response is flushed
- A database query timeout causing the handler to exit early
- An intermediate load balancer (like an AWS ALB) with a shorter idle timeout than Nginx
To isolate this, bypass Nginx and hit the backend directly with curl -v http://backend-host:port/your-endpoint. If curl also gets a truncated or aborted response, the problem is in your application, not Nginx. Fix it there first.
Common Pitfalls
Changing buffers in the wrong context. Nginx directive inheritance means a setting in http {} applies everywhere, but a setting in a nested location {} overrides it. If you set generous buffers globally but then have a location block that resets proxy_buffering off, the location block wins. Always verify the effective configuration with nginx -T | grep proxy_buffer.
Forgetting to reload after editing. nginx -s reload performs a graceful reload. It does not interrupt in-flight requests, so if your test immediately after the edit still fails, it may be because existing worker processes are still handling that connection. Give it a few seconds or test with a fresh request.
Confusing proxy_read_timeout with request timeout. This timeout resets on every successful read from upstream. A backend that sends headers quickly and then slowly streams a large body will not hit this timeout as long as bytes keep arriving. If you're seeing truncation at exactly 60 seconds on slow streams, the backend is genuinely pausing for a full minute between writes β which is a backend bug, not an Nginx tuning problem.
Temp directory permissions after a system update. Package manager updates occasionally reset the permissions or ownership of Nginx cache directories. After an OS or Nginx package upgrade, verify the temp path is still writable.
Verifying the Fix
After making changes, verify with a targeted test. Use curl and compare the Content-Length header with the actual bytes received:
curl -so /dev/null -D - https://your-domain/api/large-endpoint \
-w "bytes_downloaded: %{size_download}\nhttp_code: %{http_code}\n"The size_download value should match the Content-Length in the response headers. If your backend sends chunked transfer encoding without a Content-Length, you'll need to compare against a known reference β either capture a direct backend response or check your application's own logs for how many bytes it wrote.
Also watch your custom access log for a few minutes of real traffic. The ups_bytes field should now be consistent and not suspiciously small on the requests that were previously truncated.
Wrapping Up
Silent truncation from Nginx usually comes down to one of three things: buffers that are too small, timeouts that are too short, or a backend that closes connections before it should. Here's what to do next:
- Add
upstream_response_lengthandupstream_response_timeto your Nginx access log format right now β even if you're not debugging a problem. This data is invaluable when something breaks at 2am. - Audit your
proxy_buffer_sizeandproxy_buffersagainst the actual response sizes your upstream returns. If you don't know what those sizes are, check your application metrics or add aContent-Lengthlog to your backend. - Test your backend directly with
curlbefore blaming Nginx. If the issue reproduces without Nginx in the path, fix the application first. - Review your
proxy_read_timeoutfor any endpoints that stream data or run long queries. Match the timeout to the realistic worst-case gap between upstream writes, not the total response duration. - After any Nginx or OS package update, verify that your proxy temp directory still exists and has correct ownership for the Nginx worker process.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!