Worker processes vs worker connections in NGINX

NGINX
Concurrency
Kubernetes
The two separate knobs that determine NGINX concurrency ceiling, why they multiply rather than add, and what actually caps them in a container.
Author

Lahiru De Silva

Published

September 6, 2026

NGINX has one master process and a pool of worker processes, sized by worker_processes. Each worker runs its own single-threaded, event-driven loop and can hold up to worker_connections open sockets at once. The two directives answer different questions, worker_processes is how many event loops run, worker_connections is how full each one can get, and the real system ceiling is worker_processes × worker_connections.

%%{init: {"flowchart": {"subGraphTitleMargin": {"top": 10, "bottom": 15}}}}%%
flowchart TB
    M["Master process<br/>reads config, binds sockets,<br/>forks workers"]

    subgraph W0["Worker process 1 · event loop"]
        direction TB
        E0["epoll_wait()"]
        C0["up to worker_connections<br/>open sockets"]
        E0 --> C0
    end

    W1["Worker process 2"]
    W2["Worker process 3"]
    W3["Worker process 4"]

    M --> W0
    M --> W1
    M --> W2
    M --> W3

worker_processes: how many event loops

worker_processes auto;

auto picks one worker per CPU core visible to the OS. On bare metal or a dedicated VM that’s the right number. One worker per core keeps every loop busy without workers fighting each other for CPU time.

In a container that number is not accurate. The count NGINX reads is the host’s online CPUs, not the cgroup CPU quota the container is actually limited to. A pod with limits.cpu: "1" sitting on a 64-core node still gets worker_processes auto resolving to 64. Unlike Envoy concurrency autodetection, which reads the cgroup quota as one of the terms it takes a minimum over, stock NGINX has no cgroup awareness at all here. Every one of those 64 workers is a standing cost, its own memory, its own idle event loop, competing for CPU time the container was never granted, and getting throttled instead of simply sitting idle. Set the number explicitly rather than trusting autodetection.

worker_processes 1;   # matches a 1-vCPU limit, not the node's core count

worker_connections: how full each loop can get

events {
    worker_connections 1024;
}

This caps sockets per worker, not requests per worker, and the two cases spend that cap differently. Serving a static file holds one socket per client, so a worker at capacity serves close to the full worker_connections value in concurrent requests. A proxied request holds two, one to the client, one to the upstream, so a worker proxying at capacity serves roughly half that value instead.

worker_connections also can’t exceed whatever file descriptor limit the worker actually has. worker_rlimit_nofile sets that ceiling explicitly rather than inheriting the process’s default, which is often 1024 and leaves almost no headroom above the connections themselves:

worker_rlimit_nofile 4096;   # comfortably above worker_connections

Undersize it and workers start logging accept() failed (24: Too many open files) and refuse new connections while still holding the ones they have.

Max clients

The two directives compose as a product:

max clients ≈ worker_processes × worker_connections

worker_processes 4 and worker_connections 1024 is a theoretical ceiling of 4096 connections. For proxied traffic, halve that to get a realistic concurrent-request number, then plan around a peak well below even that, since a retry storm or a slow upstream holding connections open longer than usual will chew through the remaining margin quickly.

WarningThe kernel has its own ceiling in front of all of this

Every one of those connections still has to sit in the kernel accept queue before NGINX ever sees it. The listen directive backlog and net.core.somaxconn cap that accept queue, and the smaller of the two wins regardless of how large worker_connections is set. A queue that fills before a worker calls accept() drops the connection silently, visible only as TcpExtListenOverflows in kernel counters, not in any NGINX log.

Checking what’s actually running

nginx -T | grep -E 'worker_processes|worker_connections|worker_rlimit_nofile'

confirms the configured values, and on Linux, the number of forked workers is verifiable directly against the config:

ps -e -o pid,cmd | grep '[n]ginx: worker process' | wc -l

That count should match worker_processes, and on a Kubernetes node it’s worth checking against the pod limits.cpu too, not just the number that looked right in the config file.