RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Nginx Worker Connections Exhaustion Fix
02. Root Cause Vector

Nginx exhausts its file descriptor limit under load, causing accept() failures and a complete request blackout.

03. Rapid Prescription

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

Zero-Trust Client IDE/ Performance

Nginx Worker Connections Exhaustion Fix

Nginx exhausts its file descriptor limit under load, causing accept() failures and a complete request blackout.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(58L)
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: refactor this mess after PCI audit next quarter
# WARNING: This config is a temporary patch for legacy-api.internal/nginx-proxy:v1.2.3
# DO NOT MODIFY WITHOUT APPROVAL FROM SECURITY TEAM

worker_processes auto;
worker_rlimit_nofile 1024; # VULNERABILITY: Hardcoded low limit conflicts with high connection demands

events {
    # TODO: fix this workaround - we increased workers but not file limits
    worker_connections 2048; # This will cause accept() failures under load
}

http {
    # Legacy PCI zone config - DO NOT REMOVE
    include /etc/nginx/conf.d/legacy-pci-headers.conf;
    # include /etc/nginx/conf.d/rate-limit.conf; # commented out for performance
    
    upstream backend_servers {
        server my-registry.internal/legacy-api:8080 max_fails=3;
        server pci-secure-zone/api-gateway:8080 backup;
        # server old-load-balancer.internal:8080 down; # decommissioned last year
    }
    
    server {
        listen 443 ssl;
        server_name api.corporate-app.internal;
        
        # AWS keys for S3 logging - rotate quarterly
        set $aws_access_key "AKIAIOSFODNN7EXAMPLE";
        set $aws_secret_key "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
        
        # Stripe webhook secret
        set $stripe_webhook_secret "whsec_5f1a3e8b7c9d2a4b6c8d0e2f4a6b8c9d";
        
        # Multi-line RSA key for internal service auth
        set $legacy_private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArL7Fqk6N7bZqXzQwQ9XvJ8KpLmR5tYyHjKlP9oMnTWVx1\n2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2\n-----END RSA PRIVATE KEY-----";
        
        # Database URI with credentials
        set $postgres_uri "postgresql://app_user:S3cur3P@ssw0rd!@pci-db.internal:5432/legacy_db";
        
        location /api/v1/ {
            proxy_pass http://backend_servers;
            proxy_connect_timeout 10s;
            proxy_read_timeout 30s;
            # TODO: This buffer setting causes memory issues under high load
            proxy_buffers 32 4k;
        }
        
        # Health check endpoint
        location /health {
            access_log off;
            return 200 "OK\n";
        }
    }
}

# TEMPORARY FIX: Increased keepalive timeout to reduce connection churn
# keepalive_timeout 75; # Actually makes things worse with limited file descriptors
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix Nginx 'worker_connections are not enough: accept() failed (24: Too many open files)'

Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins

TL;DR

  • What broke: Nginx hit the OS file descriptor ceiling (ulimit -n). Every new TCP connection requires an fd. When the pool is exhausted, accept() hard-fails and Nginx drops all incoming requests.
  • How to fix it: Raise worker_rlimit_nofile in nginx.conf, sync it with the systemd LimitNOFILE override, and recalculate worker_connections to match.
  • Use our Client-Side Sandbox below to paste your nginx.conf and auto-refactor the worker and fd directives without sending your config off-box.

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

The Incident (What Does the Error Mean?)

Raw log output from /var/log/nginx/error.log:

2024/07/15 03:42:17 [alert] 1234#1234: worker_connections are not enough: accept() failed (24: Too many open files)
2024/07/15 03:42:17 [alert] 1234#1234: *8821903 open() "/var/cache/nginx/..." failed (24: Too many open files)

Errno 24 is EMFILE — the process-level open file descriptor table is full. Nginx cannot open a new socket for the incoming connection. The request is silently dropped. No 502, no 503 — the TCP handshake completes at the kernel level but Nginx immediately closes it. Clients see a connection reset. Monitoring that only checks HTTP status codes will miss this entirely.


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

The Attack Vector / Blast Radius

This is a cascading capacity failure, not a single-point event:

  1. Each Nginx connection consumes at minimum 1 fd (client socket). Proxying to an upstream doubles it. Serving a file adds another. A single HTTP/1.1 keepalive session with assets can hold 3–5 fds simultaneously.
  2. Default ulimit -n on most Linux distros is 1024. A single Nginx worker with worker_connections 1024 will saturate this under moderate load — the math doesn't work unless you explicitly raise the OS limit.
  3. Systemd overrides the shell ulimit. Even if you set ulimit -n 65535 in /etc/security/limits.conf, a systemd-managed Nginx process ignores it unless LimitNOFILE is set in the service unit. This is the #1 reason the fix appears to work in testing and fails in production.
  4. Blast radius: 100% of new inbound connections fail. Active keepalive connections already established continue until they close. Traffic spikes (deploys, marketing events, bot crawls) trigger the threshold unpredictably. The worker process does not crash — it keeps running and logging alerts at high frequency, which can itself cause disk I/O pressure.

How to Fix It (The Solution)

Step 1 — Diagnose current limits

# Check the running Nginx worker's actual fd limit
NGINX_PID=$(pgrep -o nginx)
cat /proc/$NGINX_PID/limits | grep 'open files'

# Check current system-wide fd usage
cat /proc/sys/fs/file-nr
# output: <allocated> <freed> <max>

Step 2 — Basic Fix: `nginx.conf`

# /etc/nginx/nginx.conf

- worker_processes  1;
+ worker_processes  auto;  # matches CPU core count

+ worker_rlimit_nofile 65535;  # MUST be set; raises per-worker fd limit

  events {
-     worker_connections  1024;
+     worker_connections  16384;
+     use epoll;           # Linux: explicit epoll for performance
+     multi_accept on;     # accept all pending connections per epoll event
  }

  http {
      keepalive_timeout  65;
+     keepalive_requests 1000;
  }

Rule of thumb: worker_connections × worker_processes × 2 (upstream fd) must be less than worker_rlimit_nofile × worker_processes.

Step 3 — Enterprise Best Practice: Systemd Unit Override

Without this, Step 2 alone will not hold under systemd.

systemctl edit nginx
# /etc/systemd/system/nginx.service.d/override.conf

+ [Service]
+ LimitNOFILE=65535
+ LimitNOFILESoft=65535
systemctl daemon-reload
systemctl restart nginx

# Verify — must show 65535
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'

Step 4 — OS-Level Kernel Tuning (for >10k concurrent connections)

# /etc/sysctl.conf

+ fs.file-max = 2097152
+ net.core.somaxconn = 65535
+ net.ipv4.tcp_max_syn_backlog = 65535
sysctl -p

💡 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. Nginx config linting in your pipeline:

# In your CI step — catches syntax errors before deploy
nginx -t -c /path/to/nginx.conf

2. Checkov policy for Nginx fd limits (custom check):

# checkov custom check: ensure worker_rlimit_nofile >= 65535
from checkov.common.models.enums import CheckResult

def check_nginx_rlimit(config_text):
    import re
    match = re.search(r'worker_rlimit_nofile\s+(\d+)', config_text)
    if not match or int(match.group(1)) < 65535:
        return CheckResult.FAILED
    return CheckResult.PASSED

3. Ansible enforcement — ensure the systemd override is always present:

- name: Set Nginx LimitNOFILE via systemd override
  ansible.builtin.copy:
    dest: /etc/systemd/system/nginx.service.d/override.conf
    content: |
      [Service]
      LimitNOFILE=65535
  notify:
    - daemon-reload
    - restart nginx

4. Alerting — add a Prometheus alert on fd exhaustion before it hits 100%:

# prometheus/alerts.yml
- alert: NginxFdExhaustionImminent
  expr: nginx_connections_active / nginx_worker_connections_limit > 0.80
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "Nginx fd pool >80% utilized — preemptive action required"

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 raising worker_connections in nginx.conf not fix the 'Too many open files' error?
Because worker_connections controls Nginx's internal connection queue, but the actual file descriptor ceiling is enforced by the OS at the process level. If worker_rlimit_nofile in nginx.conf and LimitNOFILE in the systemd service unit are not also raised to match, the kernel will still reject new fd allocations with EMFILE (errno 24) regardless of what Nginx is configured to accept. This is a classic multi-layer failure that requires precise, synchronized tuning across both application and OS configuration—a single oversight leaves your edge layer crippled.
Q2.What is the correct formula for calculating worker_connections relative to worker_rlimit_nofile?
Each proxied connection uses 2 fds (client socket + upstream socket), plus Nginx needs fds for log files, config, and cache. A safe formula: worker_rlimit_nofile >= (worker_connections * 2) + 100. For worker_connections 16384, set worker_rlimit_nofile to at least 33000, rounded up to 65535 for headroom. But manually calculating and applying this is error-prone; a miscalculation or misconfiguration can open security vulnerabilities or cause cascading failures. Instead of risking a production outage, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that enforces the correct limits without exposing attack surfaces.
Q3.Does this error cause Nginx to crash or restart the worker process?
No. The worker process stays alive and continues serving already-established connections. Only new accept() calls fail. This makes the outage invisible to health checks that test an existing persistent connection or a loopback probe—the process appears healthy while all new external traffic is being dropped. At scale, across large mono-repos or distributed environments, manually identifying and remediating these silent failures becomes a compliance and operational nightmare. For automated, consistent, and auditable remediation across your entire CI/CD pipeline, upgrade to the StackEngine CI/CD Automator (Team Edition) for on-prem, automated fixes that ensure compliance and eliminate drift.