Logo

Tune Nginx for high load

Tune Nginx for high load

Configure Nginx to serve thousands of simultaneous connections. This guide covers workers, keepalive, gzip/brotli compression, caching and TLS/HTTP2 optimizations for a production VPS.

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

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members