Worker Process #

Nginx’s legendary speed and lightweight workload don’t happen by accident. Unlike traditional web servers that create a new process or thread for every user connection, Nginx adopts an asynchronous event-driven architecture. The core of this architecture is driven by the Worker Process.

Understanding and aligning worker process configuration with our server’s hardware architecture is the absolute foundation of optimizing Nginx performance. Wrong tuning can cause our server to run out of file descriptor allocations, fail to utilize multi-core CPUs to the fullest, or suffer response time degradation during traffic bursts. In this article, we’ll thoroughly unpack Nginx’s master-worker architecture, how to determine server connection capacity, techniques for bypassing operating system file descriptor limits using worker_rlimit_nofile, CPU affinity configuration, and putting together a global configuration file ready for large-scale production use.

Nginx Master-Worker Architecture #

Nginx operates using a highly structured multi-process model, separating administrative responsibilities from actual traffic processing. This structure consists of one Master Process and one or several Worker Processes.

Here’s a diagram of the Master-Worker relationship in Nginx:

flowchart TD
    OS["Operating System / Network Socket (Port 80/443)"] -->|Accepts Connections| Master["Master Process (Root)"]
    Master -->|"Manages & Monitors (Spawn/Reload)"| Worker1["Worker Process 1 (www-data)"]
    Master -->|"Manages & Monitors (Spawn/Reload)"| Worker2["Worker Process 2 (www-data)"]
    Master -->|"Manages & Monitors (Spawn/Reload)"| Worker3["Worker Process 3 (www-data)"]

    Worker1 -->|"Event Loop (epoll) - Handles Thousands of Requests"| Clients1("HTTP Clients")
    Worker2 -->|"Event Loop (epoll) - Handles Thousands of Requests"| Clients2("HTTP Clients")
    Worker3 -->|"Event Loop (epoll) - Handles Thousands of Requests"| Clients3("HTTP Clients")

    classDef default fill:#f9f9f9,stroke:#d1d5db,stroke-width:1px,color:#111827;
    classDef masterStyle fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#991b1b;
    classDef workerStyle fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a;
    
    class Master masterStyle;
    class Worker1,Worker2,Worker3 workerStyle;

1. Master Process #

The master process is the parent of all Nginx processes. It runs with high-level access rights (root) because it needs the authority to bind low-numbered network ports (like HTTP port 80 and HTTPS port 443) and read sensitive SSL certificate files.

The master process’s main duties include:

  • Reading and validating the Nginx configuration file (nginx.conf).
  • Opening and holding network port sockets.
  • Spawning, monitoring, and reloading worker processes.
  • Receiving system control signals (like zero-downtime config reload via systemctl reload nginx or safe shutdown).

The master process never handles client HTTP requests directly. It fully delegates that processing task to worker processes.

2. Worker Process #

Worker processes run with restricted user access rights (by default using the www-data or nginx user) for security reasons. If a security hole is exploited on the web application side, the attacker only gets those restricted user access rights, not root access.

The worker process’s main duties include:

  • Accepting and processing incoming client connections.
  • Reading and writing data to disk (if serving static files or caching).
  • Communicating with upstream backend servers as a reverse proxy.
  • Managing SSL/TLS encryption/decryption.

The Non-Blocking Event Loop Model (epoll/kqueue) #

To understand why one Nginx worker thread can handle tens of thousands of simultaneous connections, we must compare it to the traditional multi-threaded model used by servers like Apache (in standard prefork/worker mode).

On traditional servers, each client connection is allocated to its own process or thread. When that thread waits for data from a database or disk, the thread enters a blocked state. The server must create hundreds or thousands of threads to serve many users. This causes huge RAM waste and burdens the CPU due to context switching activity (the CPU process switching tasks between threads, which is very intensive).

Nginx cuts that overhead using a Single-Threaded Event Loop in every worker process. Each worker runs on a single continuous thread and uses asynchronous (non-blocking) system calls like epoll on the Linux kernel or kqueue on BSD/macOS.

When a request arrives and must wait for a response from a backend server, the Nginx worker doesn’t just stand frozen waiting. The worker registers that connection to the event loop, then immediately switches to processing requests from other clients whose data is already ready to send. When the backend finishes processing the data and sends the response back, the event loop notifies the Nginx worker to complete that first request. This way, thousands of connections can be handled in turn by a single thread with minimal memory overhead.


Determining the Number of Workers (worker_processes) #

The worker_processes directive determines how many worker processes the Nginx master process will create. This directive is written outside the http context block (at the global/main level).

# Global nginx.conf configuration
worker_processes auto;

Why auto? #

The best value for almost all production scenarios is auto. With auto, Nginx automatically detects the number of physical CPU cores available on our server and creates exactly one worker process per CPU core.

Having one worker per CPU core is ideal because:

  • It avoids the overhead of moving thread execution between CPU cores (context switching).
  • It ensures each CPU core works in parallel independently without competing for resources.

How to Check the Server’s CPU Core Count #

We can check the number of CPU cores installed on our Linux server with the following CLI commands:

# Display the CPU core count
nproc

# Or view detailed CPU information
grep -c processor /proc/cpuinfo

If the nproc command returns 8, then with the worker_processes auto configuration, Nginx will run 8 worker processes automatically.


Setting the Connection Limit per Worker (worker_connections) #

The worker_connections directive determines the maximum number of simultaneous connections one worker process can handle at once. This directive is written inside the events context block.

events {
    # Maximum connections per worker
    worker_connections 10240;
}

Calculating the Server’s Maximum Connection Capacity #

The theoretical maximum simultaneous connection capacity our server can handle is calculated with the formula:

$$\text{Max Capacity} = \text{worker_processes} \times \text{worker_connections}$$

For example, if our server has 4 CPU cores (worker_processes 4) and we set worker_connections to 10240, then the server’s maximum connection capacity is:

$$4 \times 10240 = 40,960 \text{ simultaneous connections}$$

Real Capacity in Reverse Proxy Scenarios #

Note that the number above is the theoretical connection capacity. In the real world, if our Nginx is configured as a Reverse Proxy (forwarding traffic to backends like Node.js/PHP), one client request consumes two connections on the Nginx side:

  1. One connection between the client (browser) and Nginx.
  2. One connection between Nginx and the upstream backend server.

Therefore, the effective simultaneous request capacity on a reverse proxy is halved from the theoretical capacity:

$$\text{Effective Request Capacity} = \frac{\text{worker_processes} \times \text{worker_connections}}{2}$$

With the example above, our server can serve about $20,480$ simultaneous requests at once.


Overcoming the File Descriptor Bottleneck: worker_rlimit_nofile #

In the Linux operating system, everything is represented as a file—including network connections (network sockets). Every time a client connects to our server, the OS allocates a unique identifier called a File Descriptor (FD).

By default, Linux applies a limit on the number of file descriptors a single process can open (usually limited to only 1024 file descriptors for non-root users). This limit is called the open files limit.

If our traffic surges and the connection count exceeds the operating system’s file descriptor limit, Nginx will start rejecting new connections and log the following error in the error log:

[crit] 4821#4821: *30412 accept4() failed (24: Too many open files)

To raise this limit without permanently changing the operating system kernel configuration, we use the worker_rlimit_nofile directive at Nginx’s global level. This directive forces the operating system to give a larger file descriptor limit specifically to Nginx worker processes.

# Set the file descriptor limit for all Nginx worker processes
worker_rlimit_nofile 65535;

events {
    # Now we can safely set high connections because the rlimit has been raised
    worker_connections 10240;
}

How to Check the OS File Descriptor Limit #

We can monitor the system’s current file descriptor limits using the following terminal commands:

# Check the active file descriptor limit (soft limit) for the current user
ulimit -n

# Check the system maximum limit (hard limit)
ulimit -Hn

If we want to permanently change the system limit for the entire operating system, we must edit the /etc/security/limits.conf file and add the following lines:

# /etc/security/limits.conf
nginx       soft    nofile   65535
nginx       hard    nofile   65535

Event Loop Tuning: use epoll and multi_accept #

Inside the events context block, we can set how worker processes accept new connections to improve processing efficiency:

events {
    # Use the epoll event loop model (highly recommended for Linux)
    use epoll;

    # Allow workers to accept all new connections in the queue at once
    multi_accept on;
}

1. use epoll #

The epoll model is a very efficient event loop polling method provided by the modern Linux kernel. Unlike older methods like select or poll that scan all connections one by one looking for data (O(N) complexity), epoll uses an internal callback system that directly points to which connection is active (O(1) complexity). Nginx usually detects and selects this model automatically, but declaring it explicitly guarantees performance consistency.

2. multi_accept on #

By default, when a new connection arrives, Nginx sends a notification and the worker process only takes one new connection from the socket queue to process. By enabling multi_accept on, the worker process is instructed to take all new connections in the socket queue at once in a single event loop cycle. This is very useful for speeding up handshake time on servers experiencing sudden traffic bursts.


CPU Affinity (worker_cpu_affinity) #

In modern multi-core servers, the Linux operating system acts as a scheduler that is free to move processes from one CPU core to another to balance core temperature and workload.

However, moving Nginx worker processes between CPU cores has a negative performance impact:

  • CPU Cache Misses: Each CPU core has its own fast cache memory (L1, L2, L3). If a worker process is moved to a new core, the cache data accumulated on the old core becomes useless, and the new core must re-read data from the slower RAM.
  • Context Switch Overhead: The migration process consumes precious CPU cycles.

To lock Nginx worker processes to permanently run on specific CPU cores, we use the worker_cpu_affinity directive.

Example CPU Affinity Configuration #

Since Nginx version 1.9.10, we can hand over this affinity setting automatically to Nginx using the auto parameter:

# Nginx automatically maps each worker to its own CPU core
worker_processes auto;
worker_cpu_affinity auto;

If we want to specify core mapping manually (e.g., on a server with 4 CPU cores), we use binary representation (bitmask):

# Server with 4 CPU cores:
worker_processes 4;

# Binary bitmask mapping (Core 3, Core 2, Core 1, Core 0)
worker_cpu_affinity 0001 0010 0100 1000;
  • 0001: Worker 1 is locked only to CPU core 0.
  • 0010: Worker 2 is locked only to CPU core 1.
  • 0100: Worker 3 is locked only to CPU core 2.
  • 1000: Worker 4 is locked only to CPU core 3.

[!TIP] Using worker_cpu_affinity auto; is highly recommended because Nginx dynamically detects the CPU topology (including multi-socket NUMA architectures) and maps processes optimally without the risk of human error.


Linux Scheduler Prioritization: worker_priority #

On busy production servers, Nginx often has to share CPU resources with other system processes (like MySQL/PostgreSQL databases, logging agents, or cron jobs). If the CPU experiences high load, Nginx worker processes must queue in the Linux scheduler to get processing turns, which can slow down our web application’s responses.

We can give higher scheduling priority specifically to Nginx worker processes using the worker_priority directive.

# Give higher priority to Nginx worker processes
worker_priority -10;

This priority value refers to the nice value in the Linux scheduler:

  • The value range is from -20 (highest priority) to 19 (lowest priority).
  • The system default value is 0.
  • Setting a negative value (e.g., -5 or -10) guarantees Nginx worker processes are prioritized by the Linux scheduler when the CPU is busy, minimizing response latency during high server load.

Example Optimized Production Global Configuration #

Here’s an example of a production-level Nginx global configuration file (/etc/nginx/nginx.conf) that summarizes all the worker process optimizations we’ve discussed:

# /etc/nginx/nginx.conf
# Global / Main Context Configuration

user www-data;
pid /run/nginx.pid;

# 1. Worker and CPU Affinity Optimization
worker_processes auto;
worker_cpu_affinity auto;

# 2. Raise CPU scheduling priority
worker_priority -5;

# 3. File Descriptor Limit Optimization
worker_rlimit_nofile 65535;

# 4. Efficient Production Log Level Settings
error_log /var/log/nginx/error.log warn;

events {
    # 5. Raise per-worker connection capacity
    worker_connections 20480;

    # 6. Use the best Linux event loop model
    use epoll;

    # 7. Take queued connections in batches
    multi_accept on;
}

http {
    # Other http configuration (gzip, cache, keepalive, vhost, etc.)
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # ...
}

Connection & File Descriptor Troubleshooting Table #

When we do connection limit tuning, there’s a chance we hit other operating system limits. Here’s a troubleshooting table for common issues that often appear:

Error Message in LogLikely CauseSolution Steps
accept4() failed (24: Too many open files)The connection count exceeds the Nginx worker process’s file descriptor (FD) limit.Raise the worker_rlimit_nofile value in the Nginx configuration to at least 65535.
worker_connections exceed open file resource limitThe configured worker_connections value is larger than the system’s current ulimit -n limit.Add the worker_rlimit_nofile directive with a value higher than worker_connections times the number of workers.
Connection reset by peer in the error logThe operating system’s connection backlog queue is full due to too dense TCP handshake volume.Raise the Linux kernel somaxconn value in /etc/sysctl.conf: net.core.somaxconn = 65535.
Uneven CPU core usage (one core 100%, others 0%)CPU affinity configuration isn’t active or requests are concentrated in one worker.Make sure worker_cpu_affinity auto; is active and use even internal load balancing.

Summary and Best Practices #

  • Use worker_processes auto: Always let Nginx detect and allocate one worker process per CPU core automatically for maximum parallelism.
  • Raise the File Descriptor Limit: Write the worker_rlimit_nofile 65535 directive at the top of the configuration file to prevent the fatal “Too many open files” error.
  • Use Automatic Affinity: Enable worker_cpu_affinity auto; to lock processes to their respective CPU cores to reduce cache misses.
  • Enable multi_accept: Turn on multi_accept on; in the events block to improve new connection acceptance speed during traffic bursts.
  • Give CPU Priority: Set worker_priority -5 on dedicated production servers to keep Nginx response stability under high CPU load conditions.

← Previous: Log Rotation   Next: Gzip Compression →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact