Nginx 502: "Upstream Closed Prematurely" — Crashed or Timing Out Backend

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.

What you'll see

Root causes

Upstream crashing on the request (check the app's logs)

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.

Keepalive disagreement: app closes idle connections nginx reuses

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.

Fix it

  1. Correlate timestamps with the app log
    sudo tail -30 /var/log/nginx/error.log | grep -i 'prematurely' ; # then same minute in the app's log; dmesg -T | grep -i oom
  2. Reproduce with a direct-to-backend request
    curl -v --max-time 10 http://127.0.0.1:3000/<failing-path> 2>&1 | tail -5   # crashes here = app bug, not nginx
  3. Align keepalive timeouts (nginx must wait longer than the app)
    # 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
  4. Give slow endpoints sane bounds
    # location /slow/ { proxy_read_timeout 120s; proxy_connect_timeout 5s; }

Field note

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.

Common questions

Why do only big requests 502 while small ones work?

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.

What does 'prematurely' actually mean here?

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.

Ship it right the first time

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.