RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Nginx WebSocket Connection Reset 104 Tracker
02. Root Cause Vector

Nginx drops WebSocket connections mid-stream with errno 104 (ECONNRESET) because the upstream application server closes the TCP socket be...

03. Rapid Prescription

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

Zero-Trust Client IDE/ Performance

Nginx WebSocket Connection Reset 104 Tracker

Nginx drops WebSocket connections mid-stream with errno 104 (ECONNRESET) because the upstream application server closes the TCP socket before Nginx finishes reading the response, typically caused by missing keepalive headers, mismatched timeouts, or upstream process crashes.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(61L)
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
# TODO: This is a temporary workaround for PCI zone WebSocket proxying until Q4 refactor.
# OLD CONFIG: upstream ws_backend { server legacy-ws-api.pci-secure-zone.internal:8443; }
# New config below, but keep old for rollback. DO NOT DELETE.

upstream ws_backend {
    server new-ws-cluster.my-registry.internal:443 max_fails=2;
    # VULNERABILITY: Upstream keepalive timeout is lower than proxy_read_timeout, causing premature connection resets under load.
    keepalive 32;
    keepalive_timeout 30s;  # Upstream may close idle connections after 30s
}

server {
    listen 443 ssl;
    server_name gateway.corp-prod.internal;
    
    ssl_certificate /etc/ssl/certs/wildcard.crt;
    ssl_certificate_key /etc/ssl/private/wildcard.key;
    
    # Hardcoded DB URI for legacy auth service (migrate to Vault soon)
    set $db_uri "postgresql://app_user:J8$kLm#9PqR2sV5xY7zA@postgres-master.pci-secure-zone.internal:5432/legacy_auth?sslmode=require";
    
    location /ws/ {
        proxy_pass https://ws_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # AWS keys for S3 logging integration (rotate quarterly)
        proxy_set_header X-AWS-Access-Key "AKIAIOSFODNN7EXAMPLE";
        proxy_set_header X-AWS-Secret-Key "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
        
        proxy_read_timeout 60s;  # Client expects 60s timeout
        proxy_send_timeout 60s;
        # TODO: Adjust buffer sizes after load tests
        proxy_buffers 16 32k;
        proxy_buffer_size 64k;
    }
    
    # Stripe webhook secret for billing microservice (deprecated but still used)
    location /stripe-webhook/ {
        set $stripe_secret "sk_live_51K8Jx2L5a9b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7";
        proxy_pass http://billing-service.internal:8080;
    }
}

# RSA private key for internal service mesh (PEM format)
# TODO: Move to secrets manager after compliance audit
# -----BEGIN RSA PRIVATE KEY-----
# MIIEowIBAAKCAQEArLJ7WqkK7zHxX8ZtN9JmKpLoPqRsTuvwX1Y2a3b4c5d6e7f8
# g9h0i1j2k3l4m5n6o7p8q9r0s1t2u3v4w5x6y7z8A9B0C1D2E3F4G5H6I7J8K9L0
# M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4a5b6c7d8e9f0g1h2i3j4k5l6m7n8o9p0q1r2
# s3t4u5v6w7x8y9z0A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4
# Y5Z6a7b8c9d0e1f2g3h4i5j6k7l8m9n0o1p2q3r4s5t6u7v8w9x0y1z2A3B4C5D6
# E7F8G9H0I1J2K3L4M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8a9b0c1d2e3f4g5h6i7j8
# k9l0m1n2o3p4q5r6s7t8u9v0w1x2y3z4A5B6C7D8E9F0G1H2I3J4K5L6M7N8O9P0
# Q1R2S3T4U5V6W7X8Y9Z0a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2
# w3x4y5z6A7B8C9D0E1F2G3H4I5J6K7L8M9N0O1P2Q3R4S5T6U7V8W9X0Y1Z2a3b4
# c5d6e7f8g9h0i1j2k3l4m5n6o7p8q9r0s1t2u3v4w5x6y7z8A9B0C1D2E3F4G5H6
# I7J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4a5b6c7d8e9f0g1h2i3j4k5l6m7n8
# -----END RSA PRIVATE KEY-----
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing Nginx WebSocket 'recv() failed (104: Connection reset by peer)' — Root Cause & Production Hardening Guide

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–30 mins

TL;DR

  • What broke: Nginx received TCP RST (errno 104) from the upstream server while proxying a WebSocket connection — the upstream killed the socket before the exchange completed, or Nginx never properly negotiated the HTTP→WebSocket upgrade.
  • How to fix it: Set proxy_read_timeout, proxy_send_timeout, add the mandatory Upgrade and Connection proxy headers, and ensure your upstream keepalive pool is correctly configured.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your nginx.conf or location block 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 the Error Mean?)

You will see this in /var/log/nginx/error.log:

2024/01/15 03:42:17 [error] 1234#1234: *8901 recv() failed (104: Connection reset by peer)
while reading upstream response header from upstream,
client: 203.0.113.45, server: api.example.com,
request: "GET /ws/live-feed HTTP/1.1",
upstream: "http://127.0.0.1:8080/ws/live-feed",
host: "api.example.com"

errno 104 = ECONNRESET. The remote peer (your upstream app server — Node.js, Gunicorn, Go binary, etc.) sent a TCP RST packet. This is not a graceful FIN-based close. It is an abrupt socket termination. The client receives a 502 Bad Gateway or a dropped WebSocket frame with no clean close handshake.

Immediate consequence: Every active WebSocket subscriber on that upstream worker loses their connection simultaneously. If you are running Socket.IO, GraphQL subscriptions, or a live-data feed, all clients reconnect at the same instant — creating a thundering herd that can cascade into a full upstream meltdown.


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

The Attack Vector / Blast Radius

This is primarily a stability and availability failure, but it has a secondary security surface:

Cascading failure path:

  1. Upstream worker OOMs, crashes, or hits its own idle timeout and sends RST.
  2. Nginx logs 104, returns 502 to all connected WebSocket clients.
  3. All clients implement exponential backoff reconnect — but if the backoff floor is low (e.g., 1s), hundreds of clients hammer the upstream simultaneously.
  4. Upstream respawns under load, immediately OOMs again. Loop.
  5. Result: Full service brownout. CPU pegged. Load balancer health checks fail. Auto-scaling triggers but new instances inherit the same misconfigured timeout.

Why the misconfiguration makes it worse:

  • Without proxy_read_timeout set high enough for long-lived WebSocket connections, Nginx itself will RST the upstream connection after the default 60 seconds of inactivity — even if the WebSocket is intentionally idle (waiting for a push event).
  • Without proxy_http_version 1.1 and the Connection: upgrade header chain, Nginx downgrades to HTTP/1.0, which has no keepalive, guaranteeing RST on every request after the first.
  • A missing or incorrect Upgrade header means the upstream never switches protocols — it responds with HTTP 200 on a non-WebSocket handler and closes the connection immediately.

Security angle: An attacker who knows your WebSocket endpoint is misconfigured can send a high-frequency reconnect flood. Because each reconnect is a full HTTP upgrade handshake, it is significantly more expensive than a plain TCP SYN flood and bypasses many L4 rate limiters.


How to Fix It

Basic Fix — Correct the WebSocket Proxy Block

The minimum viable fix for a single upstream WebSocket location:

 http {
+    map $http_upgrade $connection_upgrade {
+        default upgrade;
+        ''      close;
+    }
 
     server {
         listen 443 ssl;
         server_name api.example.com;
 
         location /ws/ {
             proxy_pass http://ws_upstream;
-            proxy_http_version 1.0;
+            proxy_http_version 1.1;
 
-            # Missing upgrade headers — upstream never switches protocols
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection $connection_upgrade;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
-            proxy_read_timeout 60s;
-            proxy_send_timeout 60s;
+            proxy_read_timeout 3600s;
+            proxy_send_timeout 3600s;
+            proxy_connect_timeout 10s;
 
+            # Disable buffering — WebSocket frames must not be buffered
+            proxy_buffering off;
+            proxy_cache off;
         }
     }
 }

Enterprise Best Practice — Upstream Keepalive Pool + Health Checks

For production environments with multiple upstream workers (Node cluster, PM2, Gunicorn with eventlet, Go net/http):

 http {
 
+    map $http_upgrade $connection_upgrade {
+        default upgrade;
+        ''      close;
+    }
 
     upstream ws_upstream {
-        server 127.0.0.1:8080;
-        server 127.0.0.1:8081;
+        server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
+        server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
 
+        # Keepalive pool — reuse TCP connections to upstream workers
+        # Prevents RST caused by connection exhaustion under load
+        keepalive 64;
+        keepalive_requests 10000;
+        keepalive_timeout 75s;
     }
 
     server {
         listen 443 ssl;
 
+        # Active health checks (requires nginx-plus or use lua-resty-healthcheck for OSS)
+        # location /nginx-health {
+        #     health_check interval=5s fails=2 passes=3 uri=/healthz;
+        # }
 
         location /ws/ {
             proxy_pass http://ws_upstream;
             proxy_http_version 1.1;
 
             proxy_set_header Upgrade $http_upgrade;
             proxy_set_header Connection $connection_upgrade;
             proxy_set_header Host $host;
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
             proxy_read_timeout 3600s;
             proxy_send_timeout 3600s;
             proxy_connect_timeout 10s;
 
             proxy_buffering off;
             proxy_cache off;
 
+            # Prevent Nginx from closing idle-but-valid WebSocket connections
+            proxy_socket_keepalive on;
 
+            # Limit concurrent WebSocket connections per IP to prevent RST flood abuse
+            limit_conn ws_conn_zone 50;
         }
     }
 
+    # Define connection zone for rate limiting
+    limit_conn_zone $binary_remote_addr zone=ws_conn_zone:10m;
 }

Upstream-side fix (Node.js example): Ensure your upstream server does not set an idleTimeout shorter than Nginx's proxy_read_timeout:

 const server = http.createServer(app);
-server.keepAliveTimeout = 5000;  // 5s — shorter than Nginx default 60s = RST
+server.keepAliveTimeout = 72000; // 72s — must exceed proxy_read_timeout
+server.headersTimeout = 75000;   // 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

Stop this from reaching production again with these enforcement layers:

1. `gixy` — Static Nginx Security & Config Linter

# Install and run in your CI pipeline
pip install gixy
gixy /etc/nginx/nginx.conf
# Catches: missing proxy headers, insecure timeout configs, buffer misconfigs

Add to your GitHub Actions workflow:

- name: Lint Nginx Config
  run: |
    docker run --rm -v $(pwd)/nginx:/etc/nginx yandex/gixy /etc/nginx/nginx.conf

2. `nginx -t` in Pre-Commit + Deployment Gate

# .github/workflows/deploy.yml
- name: Validate Nginx Config Syntax
  run: nginx -t -c ${{ github.workspace }}/nginx/nginx.conf
  # Fails the pipeline on any config error before deployment

3. Checkov — IaC Policy for Nginx Terraform/Helm Deployments

If you manage Nginx via Helm or Terraform, add a custom Checkov check:

# checkov/custom_checks/nginx_ws_timeout.py
from checkov.common.models.enums import CheckResult, CheckCategories
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck

class NginxWebSocketTimeoutCheck(BaseResourceCheck):
    def __init__(self):
        name = "Ensure Nginx proxy_read_timeout >= 3600 for WebSocket locations"
        id = "CKV_CUSTOM_NGINX_001"
        supported_resources = ['helm_release']
        super().__init__(name=name, id=id, categories=[CheckCategories.NETWORKING],
                         supported_resources=supported_resources)

4. Prometheus Alert — Catch 104 Errors Before Users Do

# prometheus/alerts/nginx.yml
groups:
  - name: nginx_websocket
    rules:
      - alert: NginxUpstreamConnectionReset
        expr: increase(nginx_upstream_connect_errors_total[5m]) > 10
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Nginx upstream RST rate elevated — check WebSocket proxy config"
          runbook: "https://wiki.internal/runbooks/nginx-104-econnreset"

5. Integration Test — Simulate Long-Lived WebSocket in CI

#!/bin/bash
# ci/test_websocket_timeout.sh
# Requires: websocat
websocat --ping-interval 30 --ping-timeout 10 \
  wss://staging.api.example.com/ws/live-feed &
WS_PID=$!
sleep 120  # Hold connection for 2 minutes
kill $WS_PID
# If websocat exits with non-zero before 120s, the test fails — timeout misconfigured

Run this in your staging smoke test suite. A properly configured Nginx+upstream stack will hold this connection for the full 120 seconds without a 104 error.

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.What is errno 104 ECONNRESET in Nginx WebSocket proxying?
Errno 104 (ECONNRESET) is a TCP-level signal of catastrophic failure, where the upstream server sends a hard RST packet, severing the connection instantly. In a WebSocket context, this isn't a network hiccup—it's a systemic failure indicating your upstream application process likely crashed, was OOM-killed, or failed to complete the HTTP-to-WebSocket protocol handshake. Nginx, acting as a proxy, is left holding the bag, forced to return a 502 Bad Gateway to every connected client and unceremoniously drop all active sessions on that upstream worker, exposing brittle infrastructure under load.
Q2.Why does Nginx reset WebSocket connections after exactly 60 seconds?
Because you're using a default timeout designed for short-lived HTTP requests on a persistent WebSocket connection. Manually adjusting `proxy_read_timeout` to hours is a ticking time bomb—you're trading connection resets for potential resource exhaustion and denial-of-service vectors, creating a massive security blast radius. Every misconfigured timeout is a new attack surface. Stop playing configuration roulette. Drop the broken Nginx manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that enforces correct timeouts while maintaining runtime security isolation.
Q3.Do I need both the Upgrade and Connection headers set in Nginx for WebSocket to work?
Yes, and missing one is a guaranteed failure. But manually ensuring these headers—alongside `proxy_http_version 1.1`—are correct across hundreds of location blocks, ingress controllers, and microservices is a compliance and audit nightmare. A single typo in a mono-repo deployment can take down real-time functionality at scale. This is precisely why manual patching fails. For automated, scalable remediation across your entire CI/CD pipeline, upgrade to the StackEngine CI/CD Automator (Team Edition). It provides on-prem analysis and automated fixes, turning this configuration minefield into a governed, repeatable process.