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.
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 processWhy 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
$ lscpuBest 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 coreworker_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/limitsConcurrency 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 ≈ 2048Practical 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 unavailableProxy 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 storageBasic 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 -plimits.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 65535Optimized 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 successfulLoad 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.45MBab 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/apiLoad 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.html502 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/nginxMonitoring 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_connectionsReference 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)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
