The Event-Driven Core, Revisited

Nginx In Depth

Chapter 2 · The Event-Driven Core, Revisited

Web Servers Fundamentals Chapter 2 described Nginx's event-driven model conceptually: a small number of worker processes, each an event loop handling many connections without a thread per connection. This chapter tunes that model for a real workload — the two directives that actually control it, the OS-level mechanism underneath, and the real ceiling those directives run into.

worker_processes

worker_processes auto;

worker_processes sets how many worker processes Nginx runs. Setting it to auto matches it to the number of available CPU cores — generally the right starting point, since each worker's own event loop runs most efficiently with its own dedicated core. Setting it too low under-uses available hardware; setting it meaningfully higher than the core count doesn't add real capacity and just adds context-switching overhead between more processes than the machine can genuinely run in parallel.

worker_connections

events { worker_connections 1024; }

worker_connections, set inside the events {} block, caps how many simultaneous connections a single worker can hold open. The real theoretical maximum number of simultaneous connections Nginx can handle is worker_processes × worker_connections — but this number is also capped by the operating system's own file descriptor limit, since every open connection consumes one file descriptor. Raising worker_connections without also raising the OS limit (worker_rlimit_nofile, and the OS's own ulimit) simply hits that ceiling instead of the number configured in Nginx.

The Event Loop in Practice

On Linux, Nginx's event loop is built on epoll, a kernel mechanism that lets a single process ask "which of these thousands of connections actually has new data ready?" without checking each one individually — the technical foundation underneath Web Servers Fundamentals' own conceptual description of "non-blocking I/O." This is what lets one worker process track thousands of mostly-idle connections cheaply, rather than needing a dedicated thread watching each one.

multi_accept and accept_mutex

Two lesser-known but real tuning directives, both inside events {}: multi_accept controls whether a worker accepts every waiting new connection in one pass or just one per event-loop cycle; accept_mutex historically serialized which worker was allowed to accept a new incoming connection, to avoid all workers waking up for the same single new connection (a "thundering herd"). Modern Nginx defaults accept_mutex to off, since modern kernels already handle this efficiently on their own — a good example of a once-important tuning knob that's since become mostly unnecessary.

DirectiveDefaultControls
worker_processesauto (recent versions)How many worker processes run
worker_connections512Max simultaneous connections per worker
worker_rlimit_nofileUnset (inherits OS limit)Max open file descriptors per worker
accept_mutexoffWhether workers serialize accepting new connections
Different tuning philosophies for different environments
On a single dedicated VPS, tuning worker_processes/worker_connections upward to use all available hardware makes sense. Inside a Kubernetes deployment (per this course's own Ingress-controller connection from Chapter 1), a single Nginx instance is often deliberately kept small and constrained — scaling out with more Pods running more Nginx instances, rather than scaling one instance up vertically, is frequently the preferred approach in that environment.
A huge worker_connections number isn't free performance
It's tempting to assume setting worker_connections to some very large number guarantees more capacity. Each held-open connection consumes real memory and one real file descriptor — a number set far beyond what the machine's own RAM and OS file-descriptor limit (ulimit -n) can actually support does nothing useful; connections simply fail once that real ceiling is hit, regardless of what the config file says. The actual capacity ceiling is worker_processes × worker_connections, capped by whatever the operating system genuinely allows — not an arbitrarily large number typed into a config file.

Hands-On Exercises

Exercise 1

A server has 4 CPU cores and worker_connections set to 1024. Calculate the theoretical maximum number of simultaneous connections this configuration allows, and name the one other real-world limit that could still cap this number lower.

📄 View solution
Exercise 2

A team sets worker_connections to 100000 on a small VPS with a default OS file-descriptor limit of 1024 per process, expecting this to significantly raise their server's capacity. Using this chapter's own warning box, explain what will actually happen.

📄 View solution
Exercise 3

Explain, in your own words, why epoll allows a single Nginx worker to efficiently track thousands of mostly-idle connections, rather than needing a dedicated thread to check each connection individually.

📄 View solution

Chapter 2 Quick Reference

  • worker_processes — usually auto, matching CPU core count
  • worker_connections — max simultaneous connections per worker; theoretical total capacity is worker_processes × worker_connections
  • Real capacity is also capped by the OS's own file descriptor limit (worker_rlimit_nofile/ulimit) — not just the config file's own numbers
  • epoll is the Linux kernel mechanism underneath Nginx's own non-blocking event loop
  • accept_mutex — off by default on modern kernels, a once-important knob that's now mostly unnecessary