RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Nginx KeepAlive Timeout Readv Reset Fix
02. Root Cause Vector

Nginx throws 502 Bad Gateway with 'readv() failed (104: Connection reset by peer)' when an upstream server closes a keepalive connection ...

03. Rapid Prescription

Verify upstream socket availability, proxy_pass hostnames, and increase proxy_read_timeout.

Zero-Trust Client IDE/ Performance

Nginx KeepAlive Timeout Readv Reset Fix

Nginx throws 502 Bad Gateway with 'readv() failed (104: Connection reset by peer)' when an upstream server closes a keepalive connection before Nginx finishes reading the response, causing silent request drops in production.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(72L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# DEPLOYMENT: legacy-api-v2 (PCI-SECURE-ZONE)
# WARNING: This config is a patchwork of three different teams. Do not refactor without approval.
# TODO: Remove after migration to new ingress controller (Q4 2025)

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-keepalive-patch
  namespace: pci-secure-zone
  labels:
    app: legacy-api
    managed-by: "terraform-helm-mess"
    # VULNERABILITY: KeepAlive timeout race condition with upstream readv reset
    # The upstream service (legacy-java-app) has a TCP idle timeout of 65s.
    # The proxy timeout is set lower, but the keepalive_timeout is higher, causing a race.
data:
  # Main nginx snippet - patched from StackOverflow 2019
  nginx-snippet.conf: |
    # Keep connections open for performance (PCI requirement)
    keepalive_timeout 75s;  # Matches old load balancer setting
    keepalive_requests 1000;
    # Proxy settings from cloud-team
    proxy_read_timeout 60s;
    proxy_send_timeout 60s;
    # TODO: Why does this cause 502s during low traffic? -Jira TICKET-7741
    # upstream legacy-java-app { ... }

  # Environment overrides (DO NOT COMMIT TO GIT - but someone did)
  env-overrides.sh: |
    export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
    export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    export STRIPE_LIVE_SECRET_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"
    # Old database URI from pre-migration
    export LEGACY_DB_URI="postgresql://user:SuperSecretPass123!@my-registry.internal:5432/pci_vault?sslmode=disable"

  # Old config fragment (commented but still referenced)
  # upstream_config_old.conf: |
  #   upstream backend {
  #     server legacy-java-app.pci-secure-zone.svc.cluster.local:8080 max_fails=3;
  #     keepalive 32;  # This is ignored by the main snippet
  #   }

---
# Helm values patch (applied on top of base chart)
# This overrides the ConfigMap but the timeout logic is still broken
apiVersion: helm.fluxcd.io/v1
kind: HelmRelease
metadata:
  name: legacy-api
spec:
  values:
    nginx:
      controller:
        config:
          # Global proxy timeout (inherited by all locations)
          proxy-read-timeout: "60"  # seconds
          # VULNERABILITY: The keepalive_timeout in the snippet (75s) > proxy-read-timeout (60s).
          # Upstream service kills idle connections at ~65s. The client holds keepalive,
          # proxy times out at 60s, but the upstream reset happens later, causing readv() 104.
          # The race is implicit between nginx, upstream TCP stack, and keepalive.
          keep-alive: "75"  # This is actually ignored; the snippet takes precedence.
        extraArgs:
          # Workaround for something else
          enable-dynamic-certificates: true
        # Embedded dummy RSA key for internal service auth (should be a secret)
        sslCertificate: |
          -----BEGIN PRIVATE KEY-----
          MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6Vf5QpC7Q8JbK
          n5z8XqLmW7vT8kYqGpRjHq1AaN2b7sP5tGvL8JxYcRfW9qKj3lMpN2b7sP5tGvL
          (FAKE_KEY_TRUNCATED_FOR_BREVITY_REAL_KEY_IS_4096_BIT)
          -----END PRIVATE KEY-----
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fix Nginx 502 Bad Gateway: readv() failed (104: Connection reset by peer) KeepAlive Timeout Mismatch

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins


TL;DR

  • What broke: Nginx is reusing a keepalive connection to an upstream (Node, Gunicorn, uWSGI, etc.) that the upstream already silently closed — Nginx reads on a dead socket and gets a TCP RST (errno 104), returning a 502 to the client.
  • How to fix it: Set keepalive_timeout on the upstream server higher than Nginx's keepalive_timeout, enable proxy_http_version 1.1, and tune keepalive pool size in the upstream {} block.
  • Fastest path: Use our Client-Side Sandbox above to auto-refactor this — paste your nginx.conf and get a corrected diff without sending your config to any external server.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does This Error Mean?)

You're seeing this in your Nginx error log:

2024/01/15 03:42:17 [error] 31#31: *18423 readv() failed (104: Connection reset by peer)
  while reading response header from upstream,
  client: 10.0.1.45, server: api.internal,
  request: "POST /api/v2/process HTTP/1.1",
  upstream: "http://127.0.0.1:8000/api/v2/process",
  host: "api.internal"

What just happened, precisely:

  1. Nginx pulled an idle connection from its upstream keepalive pool.
  2. It wrote the HTTP request headers to that socket.
  3. It called readv() to read the upstream's response.
  4. The upstream (Gunicorn/uWSGI/Node/Spring) had already sent a FIN or RST on that connection because its own keepalive timeout expired first.
  5. Nginx received TCP error code 104 (ECONNRESET) — connection reset by peer.
  6. Nginx has no valid response to proxy back → 502 Bad Gateway to your client.

Immediate consequence: Every request that lands on a stale pooled connection fails with a 502. Under load, with a large keepalive pool and a short upstream timeout, this can affect 5–30% of requests in a rolling window — enough to trigger SLA breaches and alert storms.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is a race condition at the TCP layer, not a code bug. The blast radius scales with:

Factor Why It Makes It Worse
High upstream keepalive pool size More stale connections sitting in the pool
Low upstream server keepalive timeout (e.g., Gunicorn default: 2s) Connections go stale faster than Nginx detects
HTTP/1.0 proxying (Nginx default) Connection: close forces reconnect overhead, masking the real fix
Long Nginx keepalive_timeout (default: 75s) Nginx holds connections the upstream already killed
Load balancer health checks not using HTTP/1.1 Stale connections not flushed between checks

Cascading failure scenario: Under sustained traffic, Nginx's keepalive pool fills with stale connections. Every worker that picks a stale connection wastes a request. Upstream gets hammered with new TCP connections to compensate. Connection table exhausts. You go from intermittent 502s to a full outage in under 60 seconds on a high-QPS service.

The core rule you violated (or your framework defaulted to breaking):

Nginx keepalive_timeout MUST be strictly less than the upstream server's keepalive timeout. Always. No exceptions.


How to Fix It

Basic Fix — Nginx `upstream {}` Block

The minimum viable change: force HTTP/1.1 toward upstream, enable keepalive pooling correctly, and set a safe timeout.

 http {

-    keepalive_timeout 75s;
+    keepalive_timeout 65s;  # Must be < upstream keepalive timeout

     upstream app_backend {
         server 127.0.0.1:8000;
+        keepalive 32;           # Pool size: max idle connections per worker
+        keepalive_timeout 60s;  # Drop idle upstream connections after 60s
+        keepalive_requests 1000;
     }

     server {
         listen 80;

         location / {
             proxy_pass http://app_backend;
-            # Missing HTTP/1.1 — defaults to 1.0 which sends Connection: close
+            proxy_http_version 1.1;
+            proxy_set_header Connection "";
+            proxy_connect_timeout 10s;
+            proxy_read_timeout 60s;
+            proxy_send_timeout 60s;
+            proxy_next_upstream error timeout http_502;
+            proxy_next_upstream_tries 2;
         }
     }
 }

Why proxy_set_header Connection ""? When proxying HTTP/1.1, Nginx must clear the Connection header it received from the client (which may contain keep-alive or close hop-by-hop directives) before forwarding upstream. Failing to do this causes upstreams to close connections unexpectedly.


Enterprise Best Practice — Full Hardened Config

For production: add retry logic, tune per upstream type, and set proxy_next_upstream to recover from stale connections transparently.

 upstream app_backend {
     server 10.0.1.10:8000 weight=5 max_fails=3 fail_timeout=30s;
     server 10.0.1.11:8000 weight=5 max_fails=3 fail_timeout=30s;
+    keepalive 64;
+    keepalive_timeout 55s;   # 55s < Gunicorn's 60s / uWSGI's 60s
+    keepalive_requests 2000;
 }

 server {
     location /api/ {
         proxy_pass         http://app_backend;
+        proxy_http_version 1.1;
+        proxy_set_header   Connection "";
+        proxy_set_header   Host $host;
+        proxy_set_header   X-Real-IP $remote_addr;

-        proxy_read_timeout 90s;  # Too long — masks upstream hangs
+        proxy_read_timeout 30s;
+        proxy_connect_timeout 5s;

         # Retry ONLY on connection-level errors, not on POST bodies already sent
+        proxy_next_upstream error timeout non_idempotent;
+        proxy_next_upstream_tries 2;
+        proxy_next_upstream_timeout 10s;
     }
 }

Upstream server-side fixes (you MUST also do this):

 # Gunicorn (gunicorn.conf.py)
-keepalive = 2       # Default — kills connections after 2 seconds of idle
+keepalive = 65      # Must be > Nginx upstream keepalive_timeout (55s)

 # uWSGI (uwsgi.ini)
-# http-keepalive not set
+http-keepalive = 1
+http-auto-chunked = 1
+add-header = Connection: Keep-Alive
+http-timeout = 65

 # Node.js (http/https server)
-// Default keepAliveTimeout = 5000ms (Node < 18 default)
+server.keepAliveTimeout = 65000;  // 65s — must exceed Nginx's 55s
+server.headersTimeout = 66000;    // Must be > keepAliveTimeout

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Lint Nginx Configs in Your Pipeline

# Use gixy — Nginx security/config static analyzer
pip install gixy
gixy /etc/nginx/nginx.conf

# Also: nginx -t catches syntax errors before deploy
nginx -t -c /etc/nginx/nginx.conf

2. Enforce Keepalive Rules with a Custom OPA Policy

# opa/nginx_keepalive.rego
package nginx.keepalive

deny[msg] {
    # Fail if upstream block has no keepalive directive
    upstream := input.upstreams[_]
    not upstream.keepalive
    msg := sprintf("Upstream '%v' missing keepalive pool directive", [upstream.name])
}

deny[msg] {
    # Fail if proxy_http_version is not 1.1
    location := input.locations[_]
    location.proxy_http_version != "1.1"
    msg := sprintf("Location '%v' must set proxy_http_version 1.1", [location.path])
}

3. Synthetic Monitoring — Catch Stale Pool Errors Before Users Do

# Prometheus alert — fire before 502 rate crosses SLO
- alert: NginxUpstream502Spike
  expr: |
    rate(nginx_upstream_responses_total{status="502"}[2m]) > 0.01
  for: 1m
  labels:
    severity: page
  annotations:
    summary: "502 rate from upstream exceeds 1% — check keepalive timeout mismatch"

4. Terraform / Ansible — Codify the Timeout Relationship

# variables.tf — enforce the invariant in infrastructure code
variable "upstream_keepalive_timeout" {
  default = 65
  description = "Must be set higher than nginx_upstream_keepalive_timeout"
}

variable "nginx_upstream_keepalive_timeout" {
  default = 55
  validation {
    condition     = var.nginx_upstream_keepalive_timeout < var.upstream_keepalive_timeout
    error_message = "nginx_upstream_keepalive_timeout must be strictly less than upstream_keepalive_timeout to prevent readv() 104 errors."
  }
}

The invariant to encode everywhere:

upstream_server_keepalive_timeout
  > nginx_upstream_keepalive_timeout (in upstream {} block)
    > nginx_keepalive_timeout (global http {} block)

Burn this into your runbook. Every time you add a new upstream type (Lambda via ALB, gRPC, FastAPI), verify this chain holds.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does the 'readv() failed (104: Connection reset by peer)' error only happen intermittently and not on every request?
It's a race condition. The error only fires when Nginx picks a connection from its keepalive pool that the upstream closed between the last request and this one. Under low traffic, most connections are fresh or recently used, so the upstream hasn't timed them out yet. Under high traffic or after a traffic lull, the pool fills with connections that are right at the edge of the upstream's keepalive timeout — and a percentage of them will have already received a FIN/RST from the upstream by the time Nginx tries to reuse them. The intermittency is directly proportional to how close your Nginx upstream keepalive_timeout is to (or exceeds) the upstream server's keepalive timeout.
Q2.Does setting proxy_next_upstream http_502 fully fix this, or is it just a band-aid?
It's a band-aid that prevents the 502 from reaching your clients, but it does NOT fix the root cause. With proxy_next_upstream error timeout http_502 and proxy_next_upstream_tries 2, Nginx will retry the request on a different (hopefully fresh) connection when it hits a stale one. This works for idempotent GET requests. For POST/PUT/PATCH, you must also add the non_idempotent flag and accept the risk of double-processing. The real fix is always to correct the keepalive timeout hierarchy: upstream server timeout > Nginx upstream keepalive_timeout. The retry is a safety net, not a substitute.
Q3.What are the correct keepalive timeout values for common upstream servers like Gunicorn, uWSGI, and Node.js?
Gunicorn default keepalive is 2 seconds — dangerously low. Set it to 65s minimum in production (gunicorn --keepalive 65). uWSGI requires explicitly enabling http-keepalive = 1 and setting http-timeout = 65. Node.js (pre-v18) has a default keepAliveTimeout of 5000ms (5 seconds) — set server.keepAliveTimeout = 65000 and server.headersTimeout = 66000. Spring Boot (embedded Tomcat) uses server.tomcat.connection-timeout=65000. In all cases, set your Nginx upstream keepalive_timeout to 55s and global keepalive_timeout to 65s, keeping the invariant: upstream server > nginx upstream block > nginx global.