NGINX

Tuning NGINX worker_processes: A Deep Dive

by , , revisited on


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

The worker_processes directive is one of the first things you’ll encounter in an NGINX configuration, yet it’s often misunderstood. Setting it incorrectly can leave your server underutilizing available CPU cores—or worse, thrashing due to excessive context switching.

Most tutorials simply tell you to use worker_processes auto; and move on. But what does auto actually do? When should you deviate from it? Let’s dive deep into NGINX’s source code to understand what’s really happening.

What is worker_processes?

NGINX uses an event-driven, non-blocking architecture. The master process manages configuration and worker lifecycle, while worker processes handle the actual client connections. Each worker runs an independent event loop (using epoll on Linux, kqueue on BSD/macOS) and can handle thousands of concurrent connections.

The worker_processes directive tells NGINX how many of these worker processes to spawn:

worker_processes 4;

The “auto” Value: What Happens Under the Hood

When you specify worker_processes auto;, NGINX determines the number of workers based on detected CPU cores. Looking at the source code in nginx.c:

if (ngx_strcmp(value[1].data, "auto") == 0) {
    ccf->worker_processes = ngx_ncpu;
    return NGX_CONF_OK;
}

The ngx_ncpu variable is populated at startup using platform-specific system calls:

  • Linux: sysconf(_SC_NPROCESSORS_ONLN) — returns online CPUs
  • macOS: sysctlbyname("hw.ncpu", ...)
  • FreeBSD: sysctlbyname("hw.ncpu", ...) with optional hyperthreading adjustment via machdep.hlt_logical_cpus

Key insight: On Linux, auto returns the number of online processors, which includes hyperthreads. A 4-core CPU with hyperthreading will report 8, and worker_processes auto; will spawn 8 workers.

Verify Your System’s CPU Count

Check what NGINX will see as “auto”:

# Linux - what nginx uses internally
getconf _NPROCESSORS_ONLN

# Alternative: show physical vs logical CPUs
lscpu | grep -E '^CPU(s)|Thread|Core'

# macOS
sysctl -n hw.ncpu

When “auto” Isn’t Optimal

While auto works well in most scenarios, there are cases where manual tuning is necessary:

1. Shared/VPS Hosting with CPU Limits

Cloud providers often limit CPU usage via cgroups. Your VPS might see 8 CPUs but only be allowed to use 2 cores worth of CPU time. Running 8 workers creates unnecessary overhead.

# Check if CPU limits exist (Linux with cgroups v2)
cat /sys/fs/cgroup/cpu.max

In this case, set workers to your actual CPU quota:

worker_processes 2;

2. CPU-Intensive Backends

If NGINX is primarily a reverse proxy fronting CPU-intensive applications (PHP-FPM, Python, Ruby), you may want fewer workers to leave CPU headroom for the backend:

# On an 8-core server with heavy PHP-FPM workload
worker_processes 4;

3. High-Memory Modules

Some NGINX modules (like ModSecurity, Lua, or heavy caching) consume significant memory per worker. If memory is constrained, reduce workers:

# Check per-worker memory usage
ps -C nginx -o pid,rss,cmd --sort=-rss

4. Single-CPU Systems or Containers

For minimal containers or single-CPU instances, auto correctly returns 1. However, explicitly setting it documents intent:

worker_processes 1;

Hard Limit: Maximum 1024 Processes

NGINX has a compile-time hard limit defined in ngx_process.h:

#define NGX_MAX_PROCESSES 1024

This limit covers all NGINX processes (master, workers, cache manager, cache loader). In practice, you’ll never approach this limit—systems would run out of resources long before.

By default, the OS scheduler decides which CPU runs each worker. The worker_cpu_affinity directive pins workers to specific CPUs, reducing cache misses and context-switch overhead.

Since NGINX 1.9.10, you can use auto for automatic round-robin CPU assignment:

worker_processes auto;
worker_cpu_affinity auto;

Under the hood, NGINX’s ngx_get_cpu_affinity() function distributes workers across available CPUs:

// Round-robin assign workers to CPUs
for (i = 0, j = n; ; i++) {
    if (CPU_ISSET(i % CPU_SETSIZE, mask) && j-- == 0) {
        break;
    }
}
CPU_SET(i % CPU_SETSIZE, &result);

On Linux, this ultimately calls sched_setaffinity() to bind each worker.

Manual Affinity with Bitmasks

For fine-grained control, specify CPU bitmasks per worker:

# 4 workers pinned to CPUs 0-3 (binary: 0001, 0010, 0100, 1000)
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;

# Alternative hex notation for many CPUs
worker_cpu_affinity 0x1 0x2 0x4 0x8;

NUMA-Aware Configuration

On NUMA systems, binding workers to CPUs on the same NUMA node as their memory improves performance:

# Check NUMA topology
numactl --hardware
lscpu | grep NUMA
# 2 workers per NUMA node (example: 2 nodes, 4 cores each)
worker_processes 8;
worker_cpu_affinity 00001111 11110000;

The total concurrent connections NGINX can handle is:

max_connections = worker_processes × worker_connections

The default worker_connections is 512. For busy servers:

events {
    worker_connections 4096;
}

Ensure your system’s file descriptor limit supports this:

# Check current limit
ulimit -n

# Set in nginx.conf
worker_rlimit_nofile 65535;

Practical Recommendations

Scenario Recommendation
Dedicated server, primarily serving static files worker_processes auto;
Dedicated server, reverse proxy only worker_processes auto;
VPS with CPU limits (cgroups) Set to your actual CPU quota
Server with memory-heavy modules Reduce workers, monitor memory
Shared server running NGINX + heavy backends Half of available CPUs for NGINX
Container/single-CPU worker_processes 1;

How to Verify Worker Count

After configuring, verify NGINX spawned the expected workers:

# Count worker processes
ps aux | grep 'nginx: worker' | grep -v grep | wc -l

# See all nginx processes with CPU affinity
ps -eo pid,psr,comm | grep nginx

To see which CPU each worker is bound to (requires worker_cpu_affinity):

# Linux: check affinity of each worker
for pid in $(pgrep -f 'nginx: worker'); do
    taskset -p $pid
done

Summary

  • worker_processes auto; works correctly for most dedicated servers—it spawns one worker per detected CPU (including hyperthreads)
  • On Linux, NGINX uses sysconf(_SC_NPROCESSORS_ONLN) to detect CPUs
  • Pair with worker_cpu_affinity auto; to pin workers to CPUs and reduce context switching
  • Deviate from auto when: using VPS with CPU limits, running memory-heavy modules, or sharing CPU with intensive backends
  • NGINX has a hard limit of 1024 total processes, but resource constraints will limit you long before that

For most configurations, this is all you need:

worker_processes auto;
worker_cpu_affinity auto;

events {
    worker_connections 4096;
}
D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.