The backend accepted the connection then died mid-response. Two flavors: it crashed on this specific request, or nginx and the backend disagree on keepalive timeouts.
The app dies mid-request: OOM kill, unhandled exception, segfault. The exact timestamp correlation between nginx and app logs names the endpoint. dmesg for OOM kills is step one.
If proxy_next_upstream/keepalive reuse a backend connection the app already closed (its keepalive timeout is shorter), nginx gets a half-closed socket. Classic with Node/uvicorn behind a keepalive-enabled upstream block.
sudo tail -30 /var/log/nginx/error.log | grep -i 'prematurely' ; # then same minute in the app's log; dmesg -T | grep -i oom
curl -v --max-time 10 http://127.0.0.1:3000/<failing-path> 2>&1 | tail -5 # crashes here = app bug, not nginx
# upstream { keepalive 32; } + proxy_http_version 1.1; proxy_set_header Connection ''; AND app keepaliveTimeout > nginx proxy_read_timeout is NOT needed — rather app's idle timeout > nginx's reuse window
# location /slow/ { proxy_read_timeout 120s; proxy_connect_timeout 5s; }
Don't reflexively raise every timeout: a 502 from a crashed backend needs an app fix, and papering over it hides the crash. Cloudflare in front adds a 100s ceiling — a 200s nginx timeout still surfaces as 524 upstream. Match layers.
The backend crashes or times out only on the heavy path (big upload, slow query, memory spike). Reproduce directly against the app and read ITS log — nginx is just the messenger.
The TCP connection was established, then the backend closed it before sending a complete response (headers included). Crash, exception, or idle-timeout race — the app log tells you which.
Our most-documented failures, packaged as ready-to-ship starter kits: Docker, Kubernetes, and Terraform.
Browse the template store →One-time. Yours to modify. Instant download from the NinjaOps template store.