Operations 35 min read

Nginx Configuration Optimization: Mastering Worker Processes for Performance Tuning

This guide explains Nginx's multi‑process architecture, shows how to bind worker processes to CPU cores, tune worker connections, configure upstream load‑balancing, enable proxy buffering, keepalive, gzip/Brotli compression, SSL/TLS settings, and provides testing and troubleshooting scripts for high‑performance deployments.

Raymond Ops
Raymond Ops
Raymond Ops
Nginx Configuration Optimization: Mastering Worker Processes for Performance Tuning

Process Model

Nginx uses a multi‑process architecture. The master process reads configuration, manages workers, receives signals and handles hot upgrades. Each worker process accepts client connections, processes HTTP requests, proxies to backends, handles cache and writes logs.

# View Nginx process structure
$ ps aux | grep nginx
root     12345  0.0  0.1  123456  4567   ?  Ss   10:00   0:00  nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx    12346  0.0  0.2  234567  8901   ?  S    10:00   0:02  nginx: worker process
nginx    12347  0.0  0.2  234567  8902   ?  S    10:00   0:02  nginx: worker process
nginx    12348  0.0  0.2  234567  8903   ?  S    10:00   0:02  nginx: worker process
nginx    12349  0.0  0.2  234567  8904   ?  S    10:00   0:01  nginx: worker process

Why Multi‑Process Instead of Multi‑Thread

Worker processes are independent; a crash in one does not affect others.

Each worker can run on a separate CPU core, improving parallelism.

No shared address space, simplifying memory management.

Thundering Herd Problem

Older Nginx woke all workers for a new connection; only one could acquire it.

Since Nginx 1.11.3, socket sharding (SO_REUSEPORT) distributes connections at the kernel level.

Worker Processes and CPU Binding

worker_processes

# Auto‑detect CPU cores (recommended)
worker_processes auto;

# Fixed value example
worker_processes 4;

# View CPU core count
$ nproc
8

# Detailed CPU info
$ lscpu

Best practice : use auto in production so Nginx matches the number of CPU cores. When multiple Nginx instances run on different ports, ensure the total number of workers does not exceed the core count.

worker_cpu_affinity

# 4‑core CPU binding example
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;

# 8‑core CPU binding example
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;

# 8‑core but only 4 workers
worker_processes 4;
worker_cpu_affinity 00010001 00100010 01000100 10001000;

Verify binding with:

# View process CPU affinity
$ ps -eo pid,psr,comm | grep nginx
12346  0  nginx
12347  1  nginx
12348  2  nginx
12349  3  nginx
# PSR column shows the current CPU core

worker_priority

# Set worker process priority (-20 to 20, lower value = higher priority)
worker_priority -10;

worker_rlimit_nofile

# System‑level limits (e.g., /etc/security/limits.conf)
# * soft nofile 65535
# * hard nofile 65535

worker_rlimit_nofile 65535;

File descriptor consumption sources :

Client connections (one fd per connection)

Upstream connection pool

Opened cache files

Opened log files

# View system‑level file descriptor limit
$ cat /proc/sys/fs/file-max

# View current usage
$ cat /proc/sys/fs/file-nr
1024    0       65535
# allocated  used  max

# View per‑process limits
$ cat /proc/12346/limits

Concurrency Control

worker_connections

events {
    # Default 512, production usually sets higher
    worker_connections 1024;

    # Use epoll (default on Linux)
    use epoll;

    # Accept multiple connections at once to reduce thundering herd
    multi_accept on;
}

Theoretical maximum concurrency = worker_processes * worker_connections. In practice, each connection consumes a file descriptor and upstream connections also use fds.

# Example calculation
# worker_processes = 4
# worker_connections = 1024
# Max concurrency = 4 * 1024 = 4096
# For reverse proxy, reserve half for upstream:
# Actual client connections ≈ 2048

Practical configuration examples

# General web server
worker_processes auto;
worker_connections 4096;
worker_rlimit_nofile 65535;

events {
    use epoll;
    multi_accept on;
    worker_connections 4096;
}

# High‑concurrency static server
worker_processes auto;
worker_connections 16384;
worker_rlimit_nofile 262144;

events {
    use epoll;
    multi_accept on;
    worker_connections 16384;
}

# Reverse proxy (reserve upstream connections)
worker_processes auto;
worker_connections 8192;
worker_rlimit_nofile 32768;

events {
    use epoll;
    multi_accept on;
}

Upstream Load‑Balancing Strategies

Round Robin (default)

upstream backend {
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;
}

server {
    location / {
        proxy_pass http://backend;
    }
}

Weighted Round Robin

# Equal weight example (33% each)
upstream backend {
    server 192.168.1.101:8080 weight=5;
    server 192.168.1.102:8080 weight=5;
    server 192.168.1.103:8080 weight=5;
}

# Unequal weight example (strong backend gets more traffic)
upstream backend {
    server 192.168.1.101:8080 weight=3;  # 50%
    server 192.168.1.102:8080 weight=2;  # 33%
    server 192.168.1.103:8080 weight=1;  # 17%
}

IP Hash

upstream backend {
    ip_hash;
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;
}

To temporarily remove a backend, mark it down instead of deleting.

upstream backend {
    ip_hash;
    server 192.168.1.101:8080;
    server 192.168.1.102:8080 down;  # temporarily down
    server 192.168.1.103:8080;
}

Least Connections

upstream backend {
    least_conn;
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;
}

Least Time (Nginx Plus only)

# Nginx Plus configuration
upstream backend {
    least_time header;
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
}

Full upstream example

# Complete reverse‑proxy configuration
upstream tomcat_cluster {
    # Weighted least connections
    least_conn;

    # Backend servers
    server 192.168.1.101:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 192.168.1.102:8080 weight=2 max_fails=3 fail_timeout=30s;
    server 192.168.1.103:8080 backup;  # backup server

    # Keepalive connections to backends
    keepalive 32;
}

# Parameter explanations:
# weight=N          weighted round‑robin weight
# max_fails=N      max failures before marking down
# fail_timeout=30s timeout after failure
# backup            designate as backup server
# down             permanently mark as unavailable

Proxy Buffering and Cache Settings

Buffering Principles

Client <--> Nginx Worker <--> Upstream Server
    +-- proxy_buffer_size: read response headers
    +-- proxy_buffers: read response body
    +-- proxy_busy_buffers_size: buffers during busy periods
    +-- proxy_cache: cache storage

Basic Proxy Buffer Configuration

location / {
    proxy_pass http://backend;

    # Header buffer size
    proxy_buffer_size 4k;

    # Body buffers (8 buffers of 16k each)
    proxy_buffers 8 16k;

    # Busy buffer size (used when sending to client)
    proxy_busy_buffers_size 32k;

    # Timeouts
    proxy_connect_timeout 60s;
    proxy_read_timeout 60s;
    proxy_send_timeout 60s;
}

High‑Concurrency Proxy

location /api/ {
    proxy_pass http://backend;

    # Larger buffers
    proxy_buffer_size 64k;
    proxy_buffers 16 64k;

    proxy_buffering on;

    # Disk cache size
    proxy_buffer_size 64k;
    proxy_buffers 256 16k;

    proxy_busy_buffers_size 128k;

    # No download rate limit (0 = unlimited)
    proxy_download_rate 0;
}

Disable Proxy Buffering (real‑time streaming)

location /streaming/ {
    proxy_pass http://backend;

    proxy_buffering off;
    proxy_cache off;

    # WebSocket support
    proxy_http_version 1.1;
    proxy_set_header Connection "upgrade";
}

HTTP Cache Configuration

# Define cache zone
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

location / {
    proxy_pass http://backend;
    proxy_cache my_cache;

    # Cache key
    proxy_cache_key "$scheme$request_method$host$request_uri";

    # Cache validity
    proxy_cache_valid 200 60m;
    proxy_cache_valid 404 1m;

    # Cache status header
    add_header X-Cache-Status $upstream_cache_status;

    # Bypass conditions
    proxy_cache_bypass $cookie_nocache $arg_nocache;
}

Cache Status Values

MISS – cache miss, fetch from origin

HIT – cache hit

EXPIRED – cache expired

STALE – use stale cache when origin unavailable

UPDATING – cache being refreshed

REVALIDATED – cache validated

Keepalive Connection Reuse

Upstream keepalive

upstream backend {
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;

    # Enable keepalive connections to backends
    keepalive 32;
}

location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";  # clear Connection header
}

Client keepalive

server {
    listen 80;
    server_name example.com;

    # Client keepalive timeout
    keepalive_timeout 65;

    # Max requests per keepalive connection
    keepalive_requests 1000;

    # Disable TCP slow start for high concurrency
    tcp_nopush off;
    tcp_nodelay on;
}

Keepalive Comparison

# Backend‑oriented keepalive
upstream api {
    server 127.0.0.1:8080;
    keepalive 64;
}

# Client‑oriented keepalive
server {
    keepalive_timeout 120;
    keepalive_requests 10000;
}

Gzip Compression Optimization

Basic Gzip Settings

http {
    gzip on;
    gzip_min_length 1024;
    gzip_comp_level 4;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
    gzip_vary on;
    gzip_proxied any;
    gzip_disable "msie6";
}

Detailed Gzip Parameters

http {
    gzip on;
    gzip_min_length 256;
    gzip_comp_level 4;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml
        application/xml+rss
        application/x-javascript
        image/svg+xml;
    gzip_vary on;
    gzip_proxied any;
    gzip_disable "MSIE [1-6]\.";
}

Brotli Compression (higher ratio)

# Requires Nginx compiled with --with-http_brotli_filter_module
http {
    brotli on;
    brotli_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
    brotli_comp_level 4;
    brotli_min_length 256;
}

Compression Effect Comparison

# Test gzip compression
$ curl -I -H "Accept-Encoding: gzip" http://example.com/api/data
HTTP/1.1 200 OK
Server: nginx
Content-Encoding: gzip
Vary: Accept-Encoding
Transfer-Encoding: chunked

# Size comparison (JSON API example)
# Original: 50KB
# Gzip: 12KB (76% reduction)
# Brotli: 10KB (80% reduction)

Static Resource Caching

Static File Cache Policy

server {
    listen 80;
    server_name static.example.com;
    root /var/www/static;

    # CSS/JS cache for 1 year
    location ~* \.(css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Images cache for 30 days
    location ~* \.(jpg|jpeg|png|gif|ico|webp|svg)$ {
        expires 30d;
        add_header Cache-Control "public";
        access_log off;
    }

    # Fonts cache for 1 year
    location ~* \.(woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public";
        access_log off;
    }

    # HTML no cache
    location ~* \.(html)$ {
        expires -1;
        add_header Cache-Control "no-cache, no-store";
    }
}

Static Resource Performance Optimizations

http {
    # Open file cache
    open_file_cache max=1000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # Enable sendfile and TCP optimizations
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    # Client timeouts
    client_body_timeout 12;
    client_header_timeout 12;
}

CORS Configuration

location /assets/ {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
    add_header 'Access-Control-Max-Age' 86400;

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Max-Age' 86400;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
    }

    expires 30d;
    add_header Cache-Control "public";
}

SSL/TLS Handshake Optimization

Basic HTTPS Settings

server {
    listen 443 ssl http2;
    server_name example.com;

    # Certificate files
    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # TLS protocol versions
    ssl_protocols TLSv1.2 TLSv1.3;

    # Cipher suites
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # Session cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
}

Modern SSL Configuration (TLS 1.3 only)

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    # Enable only TLS 1.3 (fastest and most secure)
    ssl_protocols TLSv1.3;

    # Modern cipher suite
    ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;

    # Certificate transparency
    ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
}

SSL Performance Tweaks

http {
    # Global SSL cache
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP cache
    ssl_stapling on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # Enable 0‑RTT (TLS 1.3) to reduce handshake latency
    ssl_early_data on;

    # Connection reuse
    keepalive_timeout 100;
}

HTTP/2 Optimization

server {
    listen 443 ssl http2;
    server_name example.com;

    # Concurrent stream limit
    http2_max_concurrent_streams 128;

    # Dynamic table size limit
    http2_recv_buffer_size 256k;
}

System‑Level Parameters

sysctl Settings

# /etc/sysctl.conf additions
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_tw_reuse = 1

fs.file-max = 2097152

# Apply changes
sysctl -p

limits.conf Settings

* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535

# Or for the nginx user
nginx soft nofile 65535
nginx hard nofile 65535

Optimized nginx.conf Template

# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
worker_priority -10;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    use epoll;
    worker_connections 65535;
    multi_accept on;
    accept_mutex off;  # modern Nginx does not need it
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" rt=$request_time uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"';
    access_log /var/log/nginx/access.log main buffer=16k flush=5s;

    # Performance parameters
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    # Timeouts
    keepalive_timeout 65;
    keepalive_requests 10000;
    client_header_timeout 15;
    client_body_timeout 15;
    send_timeout 15;

    # Buffer settings
    client_body_buffer_size 16k;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;
    client_max_body_size 50m;

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_types text/plain text/css text/xml application/json application/javascript application/xml application/xml+rss image/svg+xml;

    # Open file cache
    open_file_cache max=10000 inactive=30s;
    open_file_cache_valid 60s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # SSL
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    # Hide Nginx version
    server_tokens off;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Configuration Check and Load Testing

Syntax Check

# Test configuration syntax
$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Load Testing with wrk

# Install wrk
$ dnf install -y wrk

# Basic test
$ wrk -t12 -c400 -d30s http://127.0.0.1/

# Sample output (truncated)
Running 30s test @ http://127.0.0.1/
 12 threads and 400 connections
 Thread Stats   Avg      Stdev     Max   +/- Stdev
   Latency   5.23ms   2.15ms  45.12ms   78.45%
   Req/Sec  6.38k     523.45   12.45k    68.90%
 22953432 requests in 30.10s, 2.45GB read
Requests/sec: 762534.23
Transfer/sec: 83.45MB

ab Benchmark

# Install ApacheBench
$ dnf install -y httpd-tools

# Basic test
$ ab -n 100000 -c 1000 http://127.0.0.1/

# POST request test
$ ab -n 10000 -c 100 -p data.json -T application/json http://127.0.0.1/api

Load Test Script

#!/bin/bash
# nginx_load_test.sh – automated performance testing
TARGET_URL="${1:-http://127.0.0.1/}"
REPORT_FILE="/tmp/nginx_load_test_$(date +%Y%m%d_%H%M%S).txt"

echo "=== Nginx Performance Test ==="
echo "Target URL: ${TARGET_URL}"
echo "Test time: $(date)"

echo "[1] Low concurrency (100, 30s)..."
wrk -t4 -c100 -d30s "${TARGET_URL}" 2>&1 | tee -a "${REPORT_FILE}"

echo "[2] Medium concurrency (500, 30s)..."
wrk -t8 -c500 -d30s "${TARGET_URL}" 2>&1 | tee -a "${REPORT_FILE}"

echo "[3] High concurrency (1000, 60s)..."
wrk -t16 -c1000 -d60s "${TARGET_URL}" 2>&1 | tee -a "${REPORT_FILE}"

echo "[4] Slow connection test (100, keep alive)..."
wrk -t4 -c100 -d30s --latency "${TARGET_URL}" 2>&1 | tee -a "${REPORT_FILE}"

echo "Test completed, report saved to: ${REPORT_FILE}"

Troubleshooting Common Errors

403 Forbidden

# Check file permissions
$ ls -la /var/www/html/index.html
-rw-r--r-- 1 nginx nginx 4096 Apr 3 10:00 /var/www/html/index.html

# Fix ownership and permissions
chown nginx:nginx /var/www/html/index.html
chmod 644 /var/www/html

# SELinux context
$ ls -Z /var/www/html/index.html
chcon -t httpd_sys_content_t /var/www/html/index.html

502 Bad Gateway

# Verify upstream service
$ curl -v http://127.0.0.1:8080/health

# Check Nginx upstream configuration
$ tail -50 /var/log/nginx/error.log | grep "upstream"

# Adjust timeouts if needed
proxy_connect_timeout 60s;
proxy_read_timeout 60s;

504 Gateway Timeout

# Increase timeout values
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;

# Or investigate backend performance
upstream backend {
    least_conn;
    server 127.0.0.1:8080 weight=3;
}

Troubleshooting Script

#!/bin/bash
# nginx_troubleshoot.sh – common diagnostics

echo "=== Nginx Troubleshooting ==="

echo "[1] Nginx process status:"
systemctl status nginx | head -10

echo "[2] Configuration syntax check:"
nginx -t 2>&1

echo "[3] Port usage:"
ss -tlnp | grep -E ':80|:443'

echo "[4] Recent error log:"
tail -20 /var/log/nginx/error.log

echo "[5] Backend service check:"
curl -s -o /dev/null -w "HTTP %{http_code}" http://127.0.0.1:8080/health || echo "Backend unreachable"

echo "[6] Cache directory status:"
ls -la /var/cache/nginx/ 2>/dev/null || echo "No cache dir"

echo "[7] Disk space check:"
df -h /var/log/nginx

Monitoring Script

#!/bin/bash
# nginx_monitor.sh – status monitoring
ALERT_EMAIL="[email protected]"

check_nginx() {
    if ! systemctl is-active nginx &>/dev/null; then
        echo "CRITICAL: Nginx is not running!"
        return 1
    fi
    worker_count=$(ps aux | grep "nginx: worker" | grep -v grep | wc -l)
    if [ "$worker_count" -eq 0 ]; then
        echo "CRITICAL: No worker processes!"
        return 1
    fi
    echo "OK: Nginx running with $worker_count workers"
    return 0
}

check_connections() {
    active=$(curl -s http://127.0.0.1/nginx_status | grep "Active" | awk '{print $2}')
    reading=$(curl -s http://127.0.0.1/nginx_status | grep "Reading" | awk '{print $2}')
    writing=$(curl -s http://127.0.0.1/nginx_status | grep "Writing" | awk '{print $2}')
    waiting=$(curl -s http://127.0.0.1/nginx_status | grep "Waiting" | awk '{print $2}')
    echo "Connections: active=$active reading=$reading writing=$writing waiting=$waiting"
}

check_nginx
check_connections

Reference Information

Version

Nginx: 1.26.x (mainline) or 1.24.x (stable)

OpenSSL: 3.2.x

OS: Rocky Linux 9.4

Kernel: 6.8.5

Performance Benchmarks

Static file handling per worker: >50,000 req/s

Reverse‑proxy per worker: >10,000 req/s

TLS 1.3 handshake latency: ~50 ms

Gzip compression overhead: ~5 % CPU

Further Reading

Official Nginx documentation: https://nginx.org/en/docs/

Nginx Performance Tuning blog: https://www.nginx.com/blog/tuning-nginx/

《High‑Performance Linux Server Construction》 (Chinese)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Load BalancingPerformance TuningNginxWorker ProcessesGzipSSL
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.