Nginx High Concurrency Configuration & Performance Optimization in Practice
Nginx High Concurrency: From Architecture to Practice
Nginx's event-driven, non-blocking I/O architecture enables a single machine to handle tens of thousands of concurrent connections. However, the default configuration barely scratches the surface — deep tuning is required for production workloads.
| Optimization Area | Key Parameters | Impact |
|---|---|---|
| Process Model | worker_processes, worker_connections | Concurrency capacity |
| Event Model | use epoll, multi_accept | Event processing efficiency |
| Load Balancing | upstream, proxy_next_upstream | Traffic distribution & fault tolerance |
| Buffer & Timeout | proxy_buffers, keepalive_timeout | Memory & connection reuse |
| Compression | gzip, gzip_comp_level | Transfer bandwidth |
| Security | limit_req, limit_conn | Traffic protection |
1. Nginx Architecture: Event-Driven & Worker Processes
Master-Worker Model
Nginx uses a Master-Worker multi-process architecture:
- Master process: Reads config, binds ports, manages Workers (fork/signals/reload)
- Worker processes: Handle requests independently — one Worker crash doesn't affect others
# View Nginx process structure
# ps aux | grep nginx
# root 1234 nginx: master process
# www-data 1235 nginx: worker process
# www-data 1236 nginx: worker process
Event-Driven Model
Nginx is built on the Reactor pattern, using I/O multiplexing:
| I/O Model | Platform | Performance |
|---|---|---|
epoll |
Linux | Best, O(1) event notification |
kqueue |
FreeBSD/macOS | Excellent, similar to epoll |
select |
Universal | Poor, fd_set limit of 1024 |
poll |
Universal | Fair, no limit but linear scan |
events {
use epoll;
worker_connections 65535;
multi_accept on;
accept_mutex off;
}
multi_accept onlets a Worker accept all new connections at once, reducing system calls.accept_mutex offavoids the thundering herd problem under high concurrency (Linux 3.9+ supportsSO_REUSEPORT).
2. Core Concurrency Configuration
worker_processes
# Auto-detect CPU cores (recommended)
worker_processes auto;
# Or specify manually (usually equal to or 2x CPU cores)
worker_processes 8;
Tuning tips:
- Generally set to
auto, matching CPU core count - CPU-intensive workloads (SSL, gzip): match core count
- I/O-intensive workloads: consider 2x core count
worker_connections
events {
worker_connections 65535;
}
Max concurrent connections = worker_processes × worker_connections. You must also raise the system file descriptor limit:
# Temporary change
ulimit -n 65535
# Permanent change (/etc/security/limits.conf)
* soft nofile 65535
* hard nofile 65535
worker_rlimit_nofile
# Max file descriptors per Worker process
worker_rlimit_nofile 65535;
Complete Base Configuration
user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;
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;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 100;
}
3. Load Balancing Algorithms
Basic Configuration
upstream backend {
server 10.0.0.1:8080 weight=5;
server 10.0.0.2:8080 weight=3;
server 10.0.0.3:8080 weight=2;
server 10.0.0.4:8080 backup;
}
Five Scheduling Algorithms Compared
| Algorithm | Directive | Characteristics | Use Case |
|---|---|---|---|
| Round Robin | (default) | Sequential, even distribution | Identical backend performance |
| Weighted RR | weight=N |
Proportional by weight | Mixed backend performance |
| Least Connections | least_conn |
Prefer fewest active connections | Long-lived/varied request duration |
| IP Hash | ip_hash |
Same IP → same backend | Session persistence |
| Random | random |
Random selection | Stateless services |
| Consistent Hash | hash $key consistent |
Consistent hash ring | Cache servers |
least_conn Configuration
upstream api_backend {
least_conn;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
ip_hash Configuration
upstream web_backend {
ip_hash;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
Note:
ip_hashdoes not supportbackupandweight. Use consistent hashing in production instead.
Consistent Hash Configuration
upstream cache_backend {
hash $request_uri consistent;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
4. Upstream Health Checks
Passive Health Checks (Built-in)
upstream backend {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.3:8080 max_fails=3 fail_timeout=30s;
}
| Parameter | Meaning | Default |
|---|---|---|
max_fails |
Failure threshold before marking unavailable | 1 |
fail_timeout |
Duration marked unavailable | 10s |
Active Health Checks (nginx-upstream-check-module)
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
check interval=3000 rise=2 fall=3 timeout=1000 type=http;
check_http_send "HEAD /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
proxy_next_upstream Fault Tolerance
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_timeout 10s;
proxy_next_upstream_tries 3;
5. Buffer & Timeout Tuning
Proxy Buffers
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
| Parameter | Meaning | Recommended |
|---|---|---|
proxy_buffer_size |
Response header buffer size | 4k-8k |
proxy_buffers |
Response body buffer count & size | 8 16k |
proxy_busy_buffers_size |
Busy buffer size | Half of proxy_buffers total |
Timeout Configuration
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
client_body_timeout 12s;
client_header_timeout 12s;
send_timeout 10s;
keepalive_timeout 65s;
keepalive_requests 100;
Upstream Keepalive Connection Pool
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
keepalive 32;
keepalive_requests 100;
keepalive_timeout 60s;
}
server {
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://backend;
}
}
keepalive 32maintains 32 idle long connections per Worker to backends, avoiding frequent TCP handshakes.
6. Gzip Compression Optimization
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_http_version 1.1;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss
application/vnd.ms-fontobject
application/x-font-ttf
font/opentype
image/svg+xml;
gzip_buffers 16 8k;
gzip_disable "msie6";
| Parameter | Description | Recommended |
|---|---|---|
gzip_comp_level |
Compression level 1-9 | 4-6 (balance CPU & ratio) |
gzip_min_length |
Minimum size to compress | 1024 (small files may grow) |
gzip_types |
MIME types to compress | Add as needed, exclude images |
gzip_buffers |
Compression buffers | 16 8k |
7. Static File Serving & Caching
Static File Optimization
server {
listen 80;
server_name static.example.com;
root /var/www/static;
location / {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
open_file_cache max=10000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
expires 30d;
add_header Cache-Control "public, immutable";
}
location ~* \.(js|css)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
expires 365d;
add_header Cache-Control "public, immutable";
}
}
open_file_cache Explained
| Parameter | Meaning |
|---|---|
max=N |
Max cached file descriptors |
inactive=T |
Cache entry lifetime |
valid=T |
Interval to check cache validity |
min_uses=N |
Minimum accesses within inactive period |
errors |
Whether to cache file lookup errors |
8. SSL/TLS Performance Optimization
Session Cache
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets on;
shared:SSL:10m— all Workers share 10MB of session cache, holding ~40,000 sessions.
OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
Complete SSL Configuration
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets on;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
ssl_buffer_size 4k;
}
9. Rate Limiting & Protection
Request Rate Limiting (limit_req)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
server {
location /api/ {
limit_req zone=api_limit burst=200 nodelay;
proxy_pass http://backend;
}
}
| Parameter | Meaning |
|---|---|
rate=100r/s |
100 requests per second per IP |
burst=200 |
Allow burst of 200 requests |
nodelay |
Burst requests are not delayed; excess is rejected immediately |
Concurrent Connection Limiting (limit_conn)
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
location /api/ {
limit_conn conn_limit 50;
proxy_pass http://backend;
}
}
Bandwidth Limiting
location /download/ {
limit_rate 500k;
limit_rate_after 10m;
}
No speed limit for the first 10MB, then capped at 500KB/s.
10. Reverse Proxy Configuration
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 16k;
}
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
}
11. Common Error Troubleshooting
502 Bad Gateway
Cause: Nginx cannot connect to the backend service.
# Check if backend is running
curl -I http://10.0.0.1:8080/health
# Check Nginx error log
tail -f /var/log/nginx/error.log | grep 502
# Common causes:
# 1. Backend service not running or crashed
# 2. Backend port misconfigured
# 3. Backend overloaded, refusing connections
# 4. SELinux/firewall blocking
504 Gateway Timeout
Cause: Backend service response timeout.
# Increase timeouts
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# Troubleshooting steps
# 1. Check backend response time
curl -o /dev/null -s -w "time_total: %{time_total}\n" http://backend/api
# 2. Check backend logs for slow queries
# 3. Check network latency
ping -c 5 10.0.0.1
429 Too Many Requests
Cause: Rate limit rule triggered.
# Custom rate limit response
limit_req_status 429;
12. Security Hardening
Security Response Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
DDoS Mitigation
limit_req_zone $binary_remote_addr zone=ddos:10m rate=30r/s;
limit_conn_zone $binary_remote_addr zone=ddos_conn:10m;
server {
limit_req zone=ddos burst=50 nodelay;
limit_conn ddos_conn 30;
client_body_buffer_size 16k;
client_max_body_size 1m;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
}
Block Sensitive Paths
location ~ /\.(git|svn|env) {
deny all;
return 404;
}
location ~* /(wp-admin|phpmyadmin|adminer) {
deny all;
return 404;
}
13. Performance Benchmarking
wrk Benchmark
# Install wrk
git clone https://github.com/wg/wrk.git
cd wrk && make && sudo cp wrk /usr/local/bin/
# Benchmark (12 threads, 400 connections, 30 seconds)
wrk -t12 -c400 -d30s http://example.com/
# With latency distribution
wrk -t12 -c400 -d30s --latency http://example.com/
# POST request test
wrk -t4 -c200 -d10s -s post.lua http://example.com/api
ab Benchmark
# Install Apache Bench
# Ubuntu: apt install apache2-utils
# Benchmark (200 concurrency, 10000 requests)
ab -n 10000 -c 200 http://example.com/
# With Keep-Alive
ab -n 10000 -c 200 -k http://example.com/
Benchmark Result Analysis
| Metric | Meaning | Reference |
|---|---|---|
| Requests/sec | Requests processed per second | Static assets > 10000 |
| Latency (p99) | 99th percentile latency | < 100ms |
| Transfer/sec | Data transferred per second | Depends on workload |
| Socket errors | Connection errors | Should be 0 |
14. FAQ
Q: After setting worker_processes to auto, the Worker count doesn't match CPU cores?
A: auto reads available cores from /proc/cpuinfo. In containers with cgroup CPU limits, only Nginx 1.19+ detects cgroup constraints. Older versions need manual configuration.
Q: After setting worker_connections to 65535, actual concurrency still can't scale?
A: You must also adjust system limits: ulimit -n, net.core.somaxconn, net.ipv4.tcp_max_syn_backlog, fs.file-max.
Q: How to choose between ip_hash and consistent hashing?
A: ip_hash is simple but doesn't support weight/backup, and scaling causes mass session migration. Consistent hashing only affects adjacent nodes during scaling — recommended for cache layers.
Q: What should gzip_comp_level be set to?
A: 4-6 is the sweet spot. Levels 1-3 have low compression; 7-9 consume much more CPU for marginal gain. In practice, level 6 vs level 9 differs by only 2-3% in ratio but 50%+ more CPU.
Q: How to monitor Nginx real-time status?
A: Enable the stub_status module, then use Prometheus + Grafana or ngxtop for real-time monitoring.
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
Q: How to gracefully reload Nginx without dropping requests?
A: Use nginx -s reload. The Master reloads config, starts new Workers, and old Workers exit after finishing current requests.
Recommended tools: Use /encode/base64 to encode config files, /encode/hash to generate config hash checksums, and /json/format to format Nginx JSON logs.
Try these browser-local tools — no sign-up required →