upstream prematurely closed connection (tested)
upstream prematurely closed connection while reading response header from upstream is a line nginx writes to its error log, not something a client ever sees. It means nginx opened a connection to your app server, sent the request, and the app server's connection died before nginx got a complete set of response headers back. The client just sees 502 Bad Gateway. We have no nginx on our test box, so we rebuilt the exact failure with a Node.js reverse proxy in front of a fake upstream that closes its socket at four different points, and measured what the proxying process sees and what the client ends up with at each one.
1. What the log line actually means
Split it in two. "Prematurely closed connection" is the upstream closing the TCP connection before nginx expected it to. "While reading response header from upstream" is nginx telling you exactly which phase it was in: it had sent the request and was waiting for the status line and headers, and the socket died before that was complete. That second clause is the important part — it is not "while reading the body", it is specifically the header phase.
Because nginx had not yet seen a complete, valid response, it has committed nothing to the client. So it does the only safe thing: it can still swap in its own response, and (by default, see section 5) it sends the client a clean 502 Bad Gateway. A related line, no live upstreams, is logged instead when every server in an upstream group is already marked down from previous failures, so nginx does not even try to connect. That one is about the upstream group's health bookkeeping; this guide is about a single connection dying mid-request.
2. Our test setup
We built a minimal reverse proxy with Node's http module (no http-proxy package, just http.request piped to the client response) listening on port 8985, standing in for nginx's proxy_pass. Behind it, a fake upstream on port 8986 uses the raw net module (not http), so we can write exact, incomplete byte sequences to the socket and destroy it on command:
// upstream: 4 behaviours, selected by request path
if (path === '/no-bytes') socket.destroy(); // dies on accept
if (path === '/partial-headers') { socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/pla'); socket.destroy(); }
if (path === '/headers-no-body') { socket.write(fullHeaders); socket.destroy(); } // headers done, 0 body bytes
if (path === '/partial-body') { socket.write(fullHeaders); socket.write('short'); socket.destroy(); } // 5 of 20 promised bytes
The proxy logs the error event on its outgoing http.request, and the aborted/close events on the response object, with proxyRes.complete:
proxyReq.on('error', (err) => {
console.log('proxyReq error:', err.code, err.message);
if (!clientRes.headersSent) { clientRes.writeHead(502, ...); clientRes.end('502 Bad Gateway'); }
});
proxyRes.on('close', () => {
if (!proxyRes.complete) console.log('upstream closed mid-response, complete=false');
});
3. Results: it matters whether headers were finished
| Upstream behaviour | What the proxy's http.request saw | What curl got |
|---|---|---|
/no-bytes — dies before any byte | error event, code=ECONNRESET, message=socket hang up, 1–7 ms after connect | HTTP/1.1 502 (proxy substituted it, headers not yet committed) |
/partial-headers — dies mid status-line/headers | Same: error event, ECONNRESET, socket hang up, ~41 ms after connect — indistinguishable from no-bytes | HTTP/1.1 502 |
/headers-no-body — headers complete, 0 body bytes | Response object created (status 200 parsed OK), then aborted + close with complete=false, ~41–45 ms later | curl: (52) Empty reply from server — no 502, nothing usable reached the socket |
/partial-body — headers complete, 5 of 20 promised body bytes | Response object created, then aborted + close, complete=false, ~42–48 ms later | curl: (18) transfer closed with 15 bytes remaining to read — got short, then cut off |
/normal — full response (baseline) | close with complete=true | 200, full body |
The split at "headers complete or not" is exactly what nginx's wording is telling you. /no-bytes and /partial-headers both fail during the header phase, and both look identical to the proxying process: a plain connection reset with nothing parseable yet. That is the case nginx logs as "prematurely closed connection while reading response header from upstream", and it is also the only case where nginx can still swap in a clean 502, because it has not sent anything to the client yet.
Once headers are fully parsed, the proxy has already started committing a response to the client (Node calls writeHead as soon as it has a status line and headers). If the upstream then dies, there is no 502 to fall back on — Node's own equivalent is curl: (52) Empty reply from server for zero body bytes, or curl: (18) ... bytes remaining to read for a partial body. nginx has its own separate log line for this later failure (commonly a plain client-side reset, not the "while reading response header" message), because by then it is past the header-reading phase this guide is about.
4. What a real browser user sees
We also drove headless Chromium against the same four paths. For /no-bytes and /partial-headers it rendered the 502 body fine — a normal, fast error page. /partial-body was the interesting one: with the default page.goto(url) (which waits for the load event), navigation did not reject or resolve — it hung for the full 8000 ms timeout we set, because the page never reaches load when the body is short of its promised Content-Length. Using waitUntil: 'commit' instead, the same navigation resolved in 50 ms with status 200, because the status line and headers did arrive fine — only the body was cut short. In other words: a user who hits this mid-body case doesn't see an error, they see a tab that looks like it's still loading. Only the header-phase failures (sections 3's first two rows) turn into a clean, fast error page.
5. Common causes of the header-phase failure
- Upstream process crashed or was OOM-killed mid-request, closing its sockets. Check the app server's own log and exit status for the same timestamp.
- Worker recycling. App servers that restart workers after N requests (common in PHP-FPM, Gunicorn, uWSGI) can close an in-flight connection if a worker is recycled while it still owns a request.
- Upstream-side timeout shorter than nginx's. If the app server's own request/keepalive timeout fires before it finishes writing headers, it closes the socket from its side. Per the nginx docs,
proxy_read_timeout(default 60s) governs how long nginx itself waits for the upstream, but it does not stop the upstream from timing out first on its own, shorter setting. - Keepalive race. If nginx reuses a pooled upstream connection (
keepalivein theupstreamblock) that the app server was simultaneously closing for being idle too long, the request lands on a socket that's already going away. This shows up as sporadic failures that correlate with the upstream's idle-timeout interval. - TLS or proxy_pass misconfiguration to the wrong port can also produce a reset that looks the same, if something on that port speaks a different protocol and hangs up on the first bytes.
6. How to fix or reduce it (per the nginx docs, not measured on this box)
upstream backend {
server 127.0.0.1:9000;
keepalive 16;
}
location / {
proxy_pass http://backend;
proxy_next_upstream error timeout; # retry a different upstream server on this exact error
proxy_read_timeout 60s;
}
proxy_next_upstream error timeout;(on by default forerror) makes nginx retry the request on another server in the upstream group when this happens, instead of giving the client a 502 immediately — useful if you have more than one upstream server.- Make sure your app server's idle/keepalive timeout is longer than nginx's
keepalive_timeoutfor upstream connections, so the app server is never the one that closes a pooled connection first. - If the cause is worker recycling, set the recycling limit high enough, or make your app server finish in-flight requests before a worker exits (most have a graceful-shutdown or "max requests" grace option).
- If it correlates with deploys, it is almost always the app server's own shutdown closing in-flight connections before finishing a response — use a rolling restart with a drain period instead of a hard kill.
For the client-visible side of this (the 502 itself, retries, caching implications), see 502 Bad Gateway. If connections are failing without ever reaching the header phase, compare ERR_CONNECTION_RESET / ECONNRESET. If nginx is giving up on a slow (not dead) upstream instead, that is 504 Gateway Timeout, not this message.
Frequently asked questions
Does the client ever see the exact words "upstream prematurely closed connection"?
No. That line only goes to nginx's error log. The client gets a 502 Bad Gateway with no body text referencing it, which is what our Node proxy reproduced when the fake upstream died before sending complete headers.
Why does nginx only log this specific message during the header phase?
Because that is the only point where nginx hasn't sent anything to the client yet, so it can still substitute a clean 502. We measured this directly: once our proxy had already parsed and started forwarding headers, a later close produced a broken response (curl errors 52 or 18), not a 502, because the response was already committed.
Is this the same as 504 Gateway Timeout?
No. 504 is nginx giving up after waiting proxy_read_timeout for an upstream that is still alive but too slow. This message is the upstream's connection dying outright (crash, restart, reset) while nginx is still reading headers, which is usually much faster than a full timeout.
What's the difference from "no live upstreams"?
"No live upstreams" means every server in the upstream group is already marked down from earlier failures, so nginx doesn't even attempt a new connection. The premature-close message is a single connection that nginx did open, and that died mid-request.
Can I make nginx retry automatically instead of returning 502?
Yes, if you have more than one server in the upstream group: proxy_next_upstream error timeout; (the default for the error case) tells nginx to try the next server before giving up. With only one upstream server there's nothing to retry to, so the client still gets a 502.
Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0, curl 8.14.1, and headless Chromium 153.0.8010.12 (via Playwright), against a hand-rolled Node.js reverse proxy standing in for nginx (ports 8985/8986), fronting a raw-socket fake upstream that closes at four different points. The box has no nginx installed, so the exact nginx log wording and directive defaults are from the nginx docs, not measured; the connection mechanics (what the proxying process sees, what the client ends up with) are measured. PlainHeaders is our extension; curl, Node.js and Chromium are free.