Introduction
Default Nginx correctly serves a few hundred requests per second. With proper tuning on a modern VPS, it easily exceeds 10,000 req/s for static content and 1000-3000 req/s for dynamic content.
This guide gives essential parameters to:
- Maximize simultaneous connections
- Enable compression (gzip + brotli)
- Optimize static file cache
- Configure modern TLS (HTTP/2, OCSP stapling)
- Reduce latency
Prerequisites
- Linux VPS with Nginx 1.18+
- Root access
- Kernel optimizations already applied (see
/docs/article/kernel-tuning)
Step 1: Identify your Nginx
nginx -V 2>&1
Note compiled modules (gzip, brotli, http2, etc.). If brotli is missing on Debian/Ubuntu, install libnginx-mod-brotli:
sudo apt install -y libnginx-mod-brotli
Step 2: Configure workers
The worker is the process serving requests. On a multi-core VPS, configure:
sudo nano /etc/nginx/nginx.conf
Main block:
# Number of workers = number of CPUs
worker_processes auto;
# Increase file descriptors per worker
worker_rlimit_nofile 65535;
events {
# Simultaneous connections per worker (worker_processes × worker_connections = theoretical max)
worker_connections 16384;
# Multi-accept (accept multiple connections per event)
multi_accept on;
# epoll is best on Linux
use epoll;
}
With worker_processes auto and 4 cores: 4 workers × 16384 = 65536 max simultaneous connections.
Step 3: Global HTTP optimizations
In the http { } block:
http {
# === Basic performance ===
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# === Keepalive (TCP connection reuse) ===
keepalive_timeout 30;
keepalive_requests 1000;
# === Hash tables for faster server resolution ===
server_names_hash_bucket_size 128;
server_names_hash_max_size 4096;
types_hash_max_size 2048;
# === Buffers ===
client_body_buffer_size 16K;
client_header_buffer_size 1k;
client_max_body_size 100M; # Uploads
large_client_header_buffers 4 16k;
# === Timeouts ===
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
reset_timedout_connection on;
# === Hide Nginx version (security) ===
server_tokens off;
}
Step 4: Enable gzip compression
http {
# === Gzip compression ===
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy
text/xml;
}
gzip_comp_level 6 is the optimal balance (1 = fast/larger sizes, 9 = compact/slow).
Step 5: Enable brotli (if installed)
Brotli compresses 15-25% better than gzip for text. Supported by all modern browsers.
http {
# === Brotli compression ===
brotli on;
brotli_comp_level 6;
brotli_min_length 1024;
brotli_types
application/javascript
application/json
application/xml
application/atom+xml
application/rss+xml
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
}
If you see Content-Encoding: br in headers, brotli is active.
Step 6: Configure static file caching
http {
# === File cache (file descriptors) ===
open_file_cache max=200000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
}
And in your server { } blocks, add cache headers for statics:
server {
# ...
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* \.(pdf|zip|tar|gz)$ {
expires 7d;
add_header Cache-Control "public";
}
}
Step 7: TLS / HTTPS optimizations
http {
# === SSL ===
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # In TLS 1.3, clients choose
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
# === SSL session ===
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 4h;
ssl_session_tickets off;
# === OCSP stapling (verify cert without client round-trip) ===
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=60s;
resolver_timeout 5s;
# === HSTS ===
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
Step 8: Enable HTTP/2 and HTTP/3
In each server { listen 443 ... }:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
# HTTP/3 if Nginx 1.25+ with quic module
# listen 443 quic reuseport;
# add_header Alt-Svc 'h3=":443"; ma=86400';
# ...
}
Verify with:
curl -I --http2 https://your-domain.com
You should see HTTP/2 200.
Step 9: Reverse proxy to a backend app (PHP-FPM, Node, etc.)
Specific tuning to reduce latency:
upstream backend {
server 127.0.0.1:3000;
# Keepalive to backend (very important!)
keepalive 32;
keepalive_timeout 60s;
keepalive_requests 10000;
}
server {
location / {
proxy_pass http://backend;
# Force HTTP/1.1 for keepalive
proxy_http_version 1.1;
proxy_set_header Connection "";
# Headers to forward
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;
# Buffer response to avoid blocking backend
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 32 16k;
proxy_busy_buffers_size 32k;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Step 10: Proxy caching (very powerful for slow apps)
http {
# === Proxy cache ===
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:100m max_size=10g inactive=60m use_temp_path=off;
}
server {
location / {
proxy_cache app_cache;
proxy_cache_revalidate on;
proxy_cache_min_uses 1;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
# Add header for debug
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
# ... rest of config
}
}
Test:
curl -I https://your-domain.com
# Look for "X-Cache-Status: HIT" after 2nd call
Step 11: Rate limiting (anti-flood)
http {
# Per-IP limit
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
}
server {
# General: 30 req/s per IP, burst of 50
limit_req zone=general burst=50 nodelay;
location /login {
# Login: 5 req/min per IP (anti brute-force)
limit_req zone=login burst=2 nodelay;
# ...
}
}
Step 12: Test config and apply
sudo nginx -t
sudo systemctl reload nginx
Step 13: Benchmark
# Install wrk
sudo apt install -y wrk
# Test
wrk -t12 -c400 -d30s https://your-domain.com
Typical output:
Running 30s test @ https://your-domain.com
12 threads and 400 connections
Thread Stats Avg Stdev Max
Latency 45ms 20ms 300ms
Req/Sec 1.5k 200 2k
Requests/sec: 17500
Transfer/sec: 30MB
Troubleshooting
"worker_connections exceeded"
Increase worker_connections or worker_rlimit_nofile.
"Too many open files" error
Service doesn't respect system limits. Add to /etc/systemd/system/nginx.service.d/override.conf:
[Service]
LimitNOFILE=65535
sudo systemctl daemon-reload
sudo systemctl restart nginx
High PHP latency
Often due to badly tuned PHP-FPM. Increase pm.max_children in the PHP-FPM pool:
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
Cache not working
Check permissions:
sudo chown -R www-data:www-data /var/cache/nginx
And that your requests don't contain cookies (otherwise cache is skipped by default).
Useful commands
# Test config
sudo nginx -t
# Reload (no downtime)
sudo systemctl reload nginx
# View active connections
ss -tn | grep :443 | wc -l
# Top IPs by requests (from access.log)
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
# Real-time status (requires stub_status module)
curl http://127.0.0.1/nginx_status
# Check if gzip/brotli works
curl -I -H "Accept-Encoding: gzip, br" https://your-domain.com
Conclusion
With these optimizations, your Nginx is ready for high load:
- HTTP/2 + keepalive + brotli → -40 to -60% client bandwidth
- Proxy cache → reduces backend load by 90%+
- Rate limiting → application-level anti-flood protection
Going further:
- Compile Nginx with ngx_brotli and ngx_lua from sources
- Add Cloudflare in front to offload even more caching
- Configure WAF ModSecurity for application security
Resources
- Nginx documentation: https://nginx.org/en/docs/
- Mozilla SSL Config Generator: https://ssl-config.mozilla.org

















