This guide is maintained by Ops Error Atlas from a backend engineering perspective. It favors evidence, command output, and failure-layer separation over broad definitions or blind configuration changes.
How Ops Error Atlas reviews guidesupstream prematurely closed connection in Nginx means the upstream side closed the connection before Nginx received everything it expected for the current upstream exchange. The important part is not the phrase itself. The important part is the phase attached to it: while connecting, while sending the request, while reading response headers, or while reading the response body.
In practice this error is a boundary symptom. Nginx reports it, but the cause can be an application crash, a deploy that killed a worker too early, a mismatched keepalive timeout, a middle proxy reset, a malformed response path, or a streaming endpoint that aborted mid-body.
The useful investigation question is:
Who closed first, during which phase, and what evidence proves it?
If you cannot answer that, do not start by changing global Nginx timeouts.
What Nginx was trying to do
A typical request path is:
client -> Nginx -> upstream app -> database / cache / queue / external API
For an HTTP reverse proxy request, Nginx must:
- select an upstream peer;
- connect or reuse an upstream connection;
- send request headers and possibly a request body;
- read the upstream response headers;
- read the upstream response body;
- send a valid response to the client.
upstream prematurely closed connection means the upstream connection ended before one of those steps finished. That is why the exact error-log suffix matters.
Why this often becomes 502
The client often sees 502 Bad Gateway because Nginx cannot construct a valid response after the upstream closes too early. If Nginx has not received complete response headers, it has no valid upstream status line and headers to forward. If the upstream closes during the body, the client may see a truncated response, a reset, or a proxy-generated error depending on timing and buffering.
This is different from 504 Gateway Timeout. A 504 means Nginx waited too long. An early close means the upstream connection ended too soon. They can share root causes, but the first debugging branch is different.
Start with the full Nginx error line
Do not debug from the shortened message alone. Preserve the full log line with timestamp, upstream address, request path, and phase.
| Error-log suffix | What ended early | First branch |
|---|---|---|
while connecting to upstream | Connection failed before upstream exchange began | Listener, routing, refused/reset during connect, service discovery |
while sending request to upstream | Upstream stopped while Nginx was sending headers/body | Large request body, app not reading body, upload endpoint, upstream worker death |
while reading response header from upstream | Upstream closed before complete response headers | App crash, deploy restart, internal timeout, malformed/protocol mismatch, worker kill |
while reading upstream | Upstream closed during response body | Streaming bug, export/download abort, dependency read failure, app write timeout |
| Appears after idle periods only | Reused upstream connection was already closing | Keepalive idle timeout mismatch or middle proxy connection reuse issue |
If the suffix says “reading response header”, start with app logs, crash/restart events, and the handler path. If it says “sending request”, start with request body handling. If it clusters after idle periods, focus on keepalive reuse.
Log fields that make the error diagnosable
Add upstream address, status, timing, and a request ID to the access log. Otherwise you will struggle to connect one error-log line to one upstream request.
log_format upstream_debug
'$remote_addr request_id=$request_id '
'"$request" status=$status bytes=$body_bytes_sent '
'rt=$request_time '
'uaddr="$upstream_addr" ustatus="$upstream_status" '
'uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time" '
'host="$host" uri="$request_uri"';
access_log /var/log/nginx/access.log upstream_debug;
Interpretation:
$upstream_addrtells you which upstream peer was involved.$upstream_statusmay be empty or-if Nginx never received complete headers.$upstream_connect_timehelps separate connect/reuse issues from app handler issues.$upstream_header_timeshows how long it took to receive headers, if headers arrived.$upstream_response_timehelps distinguish header-phase failures from body-phase failures.
NGINX documents these upstream timing variables as seconds with millisecond resolution, and multiple upstream attempts can produce comma-separated values. Always pair access-log timing with the exact error-log phase.
FIN, RST, and what “closed” means
At the TCP layer, a peer can close in different ways:
| TCP signal | Practical meaning | Typical clue |
|---|---|---|
FIN | Graceful close: the peer says it is done sending | Normal shutdown, idle close, app closed after deciding no more data |
RST | Abrupt reset: the peer aborts the connection | Process crash, forced close, middle proxy reset, invalid reuse, kernel/application abort |
| Timeout/no packet | No timely progress | More likely a timeout path than an early close path |
Packet capture is not the first tool for every incident, but it is decisive when Nginx and app logs disagree about who closed first.
tcpdump -nn -i any host <upstream-ip> and port <upstream-port>
ss -tanp | grep ':8080'
If the upstream IP sends RST before Nginx receives headers, investigate app process death, forced connection close, middle proxy behavior, and protocol mismatch. If the upstream sends FIN after an idle interval and the next reused request fails, investigate keepalive timeout alignment.
Fast triage flow
Use this order before tuning anything:
- Copy the full Nginx error line, including the
while ...phase. - Identify the upstream address and whether this was a fresh or reused connection if logs allow it.
- Match the same request ID or timestamp in app logs.
- Check app restart, crash, OOM, deploy, and graceful shutdown events.
- Test the same route from the same network namespace as Nginx.
- Check whether failures cluster by upstream node, route, response size, request body size, idle interval, or deploy window.
- Use packet capture only when logs cannot prove who closed first.
The same network namespace point matters. If Nginx runs in a container or Kubernetes pod, a laptop curl test does not prove that Nginx can use the same DNS, route, network policy, and middle proxy path.
Commands that answer the first branch
Inspect the active Nginx config
nginx -T | grep -E 'proxy_pass|upstream|keepalive|proxy_http_version|proxy_set_header|proxy_(connect|send|read)_timeout|proxy_buffer|proxy_request_buffering|proxy_next_upstream'
Look for:
- whether upstream keepalive is enabled;
- whether Nginx uses HTTP/1.0 or HTTP/1.1 to upstream;
- whether
Connectionheaders are being overridden; - whether request buffering is enabled for upload routes;
- whether retry policy can hide the first failing peer;
- whether timeout settings match app behavior.
NGINX’s upstream keepalive directive keeps idle connections open for reuse. That is useful for performance, but it makes idle timeout alignment important between Nginx, the upstream app server, and any middle proxy.
Match app logs
journalctl -u your-service --since -30m
grep -R "panic\\|fatal\\|exception\\|timeout\\|SIGTERM\\|SIGKILL\\|OutOfMemory\\|killed" /var/log/your-service/
dmesg -T | grep -Ei 'killed process|oom|segfault'
Answer these questions:
- Did the app receive the request?
- Did the app finish the handler?
- Did the app log a response status?
- Did the worker restart or receive a shutdown signal?
- Did a dependency timeout or exception happen before response headers were written?
If the app logs the request and then dies before writing headers, Nginx is only reporting the symptom.
Compare fresh and reused connections
curl -v http://upstream-service:8080/problem-path
curl -H 'Connection: close' -v http://upstream-service:8080/problem-path
If forcing Connection: close reduces failures, suspect upstream keepalive reuse or idle timeout mismatch. This is a diagnostic clue, not automatically the final fix. Disabling keepalive globally can reduce a symptom while increasing connection churn and upstream accept load.
Test from Nginx’s environment
For Docker:
docker exec -it nginx-container sh
curl -v http://upstream-service:8080/problem-path
For Kubernetes:
kubectl exec -it deploy/nginx -- sh
curl -v http://upstream-service.namespace.svc.cluster.local:8080/problem-path
If the Nginx image lacks curl, use a temporary debug pod in the same namespace and network policy scope.
Case 1: App crashes or is killed before headers
Strong signals:
- Error log says
while reading response header from upstream. $upstream_statusis empty or missing.- App logs show panic, exception, worker restart, OOM kill, or forced termination.
- Failures cluster by route or input.
Likely causes:
- unhandled exception before the framework writes headers;
- process manager kills a worker after a hard timeout;
- kernel OOM killer terminates the process;
- runtime crash or segmentation fault in native extension;
- dependency timeout raises an exception that aborts the socket instead of returning a controlled 5xx.
Fix path:
- Reproduce the route directly against the upstream.
- Capture the application exception and stack trace.
- Ensure the framework returns an explicit 5xx response for handled failures.
- Set dependency timeouts below the app handler deadline.
- Add graceful error handling before response headers are committed.
The fix belongs in the application path if the app dies before producing a valid HTTP response.
Case 2: Deploy or shutdown closes active requests
Deploy-related early closes are common because process shutdown and traffic draining are often configured independently.
Typical sequence:
Nginx sends request to old worker
deployment sends SIGTERM
worker exits before response headers
Nginx logs upstream prematurely closed connection
client sees 502
Strong signals:
- Errors cluster exactly around deployments, restarts, or scaling events.
- Kubernetes logs show pod termination near the error timestamp.
- App logs show shutdown before request completion.
- Only old-version pods or draining instances appear in error logs.
Fix path:
- Fail readiness before stopping request acceptance.
- Wait for the load balancer or service discovery layer to stop routing new requests.
- Drain in-flight requests before process exit.
- Set termination grace periods to match real handler duration.
- Log shutdown reason and request IDs that were interrupted.
For Kubernetes, inspect:
kubectl describe pod <pod>
kubectl logs <pod> --previous
kubectl get events --sort-by=.lastTimestamp
kubectl rollout history deploy/<deployment>
If the issue only appears during deploys, increasing proxy_read_timeout is usually the wrong first move. The upstream is not slow; it is disappearing.
Case 3: Keepalive idle timeout mismatch
With upstream keepalive, Nginx can reuse an existing TCP connection to the upstream instead of opening a new one. This is good when both sides agree on connection lifetime. It is fragile when the upstream or middle proxy closes an idle connection and Nginx attempts to reuse it at the wrong moment.
Strong signals:
- Failures happen after idle intervals, not during steady traffic.
- Forcing
Connection: closechanges the failure rate. - Packet capture shows upstream
FINorRSTaround reused connections. - Errors are intermittent and hard to reproduce with fresh
curl.
Inspect:
nginx -T | grep -E 'upstream|keepalive|keepalive_timeout|keepalive_requests|proxy_http_version|Connection'
Fix path:
- Align idle timeouts across Nginx, middle proxies, and upstream app server.
- Ensure the upstream app server correctly supports persistent HTTP connections.
- Avoid overriding
Connectionheaders in a way that conflicts with upstream behavior. - Temporarily disable upstream keepalive only as a proof step, then choose a capacity-aware fix.
If disabling keepalive removes the error but increases CPU or connection pressure, the root cause is still timeout/lifetime alignment, not “keepalive is bad”.
Case 4: Large request body or upload route
If the error occurs while Nginx is sending the request upstream, focus on request body handling.
Strong signals:
- Error log says
while sending request to upstream. - Failures correlate with uploads, large JSON payloads, or slow client bodies.
- App logs show the request started late or not at all.
- The upstream route reads request bodies slowly or rejects them mid-stream.
Fix path:
- Route large uploads separately from normal API traffic.
- Use direct-to-object-storage uploads where appropriate.
- Make request size limits explicit.
- Ensure the app reads or rejects the body promptly.
- Align
client_body_timeout,proxy_send_timeout, and app body-read deadlines. - Review
proxy_request_bufferingonly with a clear understanding of the memory/disk and streaming tradeoff.
The wrong fix is to treat upload-body early closes like a normal slow database query. They fail in different phases.
Case 5: Response body streaming abort
If Nginx already received headers but the upstream closes during the body, the handler may have committed a response and failed later.
Strong signals:
- Error mentions
while reading upstream. - Headers arrive, then body transfer stops.
- Large exports, reports, downloads, or streaming endpoints fail more often.
- App logs show an exception after partial output.
Likely causes:
- streaming generator raises after headers are sent;
- database cursor fails mid-export;
- upstream app write timeout fires;
- dependency read stalls after partial response;
- client cancellation is not propagated cleanly to the upstream work.
Fix path:
- Move long exports to background jobs and downloadable artifacts when possible.
- Add explicit heartbeat and cancellation behavior for streams.
- Make body-generation errors visible in application logs.
- Avoid committing success headers before the operation has passed its real failure point.
- Align streaming timeouts across client, Nginx, and upstream.
Once headers are sent, retries and clean error responses become harder. That is why streaming paths need deliberate design.
Case 6: Protocol mismatch or malformed response
Some early-close cases are not crashes. They are protocol mismatches.
Examples:
- Nginx uses
proxy_pass http://...to an HTTPS-only upstream. - Nginx sends HTTP traffic to a raw TCP service.
- A gRPC-only endpoint is proxied as normal HTTP without the correct module/protocol.
- The upstream writes invalid HTTP headers and then closes.
Strong signals:
- Error alternates with
upstream sent invalid header. - Direct
curl -vshows TLS bytes, malformed headers, or empty reply. - The port is open but the protocol is wrong.
Validate from the Nginx environment:
curl -v http://upstream-service:443/path
curl -vk https://upstream-service:443/path
Fix the protocol contract before tuning timeouts, retries, or buffers.
Case 7: One upstream peer is responsible
If errors concentrate on one $upstream_addr, compare that peer with healthy peers.
Check:
- app version and deploy timestamp;
- restart count and OOM kills;
- CPU throttling and memory pressure;
- file descriptor usage;
- dependency connection pool wait;
- network retransmissions/resets;
- local disk saturation for file-heavy routes.
Remove the peer from rotation if it is actively harming traffic, but keep evidence. Draining the node is mitigation; explaining why it was bad is the fix.
Decision tree
Nginx logged upstream prematurely closed connection
|
+-- while reading response header?
| |
| +-- check app crash, worker restart, deploy shutdown, internal timeout, protocol mismatch
|
+-- while sending request?
| |
| +-- check uploads, request body reads, proxy_request_buffering, app body handling
|
+-- while reading upstream/body?
| |
| +-- check streaming/export path, partial response errors, body generation
|
+-- only after idle periods?
| |
| +-- check upstream keepalive and idle timeout alignment
|
+-- only one upstream address?
| |
| +-- compare node health, version, restarts, resource pressure, network counters
|
+-- logs disagree?
|
+-- use tcpdump to prove FIN/RST direction and timing
What not to do
- Do not assume Nginx caused the error because it logged it.
- Do not raise
proxy_read_timeoutbefore proving this is a slow-response problem. - Do not disable keepalive permanently without checking connection load and accept pressure.
- Do not ignore deploy windows; early close during shutdown is a lifecycle bug.
- Do not debug from laptop tests only; test from the Nginx network position.
- Do not lose the full error line. The
while ...suffix is the branch selector.
Minimal incident note template
Symptom:
- Client-visible status:
- Route:
- First seen:
- Upstream address:
Nginx evidence:
- exact error phrase:
- phase after "while":
- request_id:
- upstream_status:
- upstream_connect_time:
- upstream_header_time:
- upstream_response_time:
Application evidence:
- request received? yes/no
- handler finished? yes/no
- exception/crash/restart:
- deploy/shutdown event:
- dependency timeout:
Connection evidence:
- fresh curl result:
- Connection: close curl result:
- tcpdump FIN/RST direction if captured:
Hypothesis:
- who closed first:
- phase:
- evidence:
Fix:
- code/config/lifecycle change:
- validation command:
The goal is to move from “Nginx said upstream closed” to a falsifiable claim: which component closed, why it closed, and how to prove the fix.
References
- NGINX
ngx_http_proxy_module - NGINX
proxy_passdirective - NGINX
proxy_read_timeoutdirective - NGINX
proxy_request_bufferingdirective - NGINX
proxy_next_upstreamdirective - NGINX upstream keepalive directive
- NGINX upstream embedded variables
- Linux
tcp(7)manual page - tcpdump official documentation
Related errors
Move laterally when the first symptom points to adjacent network failures.
How to fix "502 Bad Gateway" from Nginx
A practical Nginx 502 guide that separates down upstreams, wrong proxy targets, protocol mismatch, early closes, malformed responses, and deploy-time failures.
Read guideWhat does "connection reset by peer" mean?
A practical guide to connection reset by peer that explains TCP resets, how to prove who sent the RST, and what to check in Linux, proxies, and upstream services.
Read guideHow to fix "broken pipe" in Linux applications
A practical guide to broken pipe errors in Linux services, focused on finding who closed first, which write path failed, and how proxies, clients, and streaming responses trigger it.
Read guideHow to debug 504 Gateway Timeout between Nginx and upstream services
A practical Nginx 504 troubleshooting guide that separates slow upstream code, dependency latency, connection problems, and unsafe timeout changes.
Read guideKeep one representative log line, the failing source and destination, the command output you used, and the verification command after the change. This makes the result reproducible and helps separate temporary recovery from a proven fix.
Browse related error guides