Complete Nginx optimization guide: from basic config to performance tuning

A modest 2-core/4GB server can handle tens of thousands of concurrent connections when Nginx is configured well — and choke on a few hundred when it isn't. The difference is not hardware; it's a few dozen lines of config. Nginx is the most popular web server and reverse proxy, powering over 30% of all websites, and is known for high performance and low resource consumption. It is effectively the de facto standard for web serving.

Basic config template

# /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;

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

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Basic optimizations
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;
}

worker_processes auto makes Nginx spawn one worker per CPU core; worker_connections times the number of cores gives you the theoretical maximum concurrent connections. Raise worker_rlimit_nofile so the file-descriptor limit keeps up, otherwise high concurrency throws "too many open files."

More workers is not always better: each process costs memory and context switches, and 8 workers on a 2-core box can actually be slower. As a rule, set workers equal to or slightly below the CPU core count, then fine-tune with load tests. To check whether the connection numbers are adequate, look at current TCP connections with ss -s: if you are approaching the worker_processes × worker_connections ceiling, raise it. Generally leave 2-3x headroom rather than blindly bumping to hundreds of thousands.

TLS configuration essentials

HTTPS is now the baseline, and TLS itself deserves its own tuning. Here is a common security baseline:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;

ssl_session_cache lets TLS sessions be reused so not every new connection needs a full handshake — especially valuable for high-concurrency APIs or pages. ssl_stapling (OCSP Stapling) caches the certificate revocation lookup in Nginx, trimming extra validation latency on the browser side.

Static file serving optimization

For Nginx serving static assets, the config below noticeably cuts bandwidth and response time:

server {
    listen 80;
    server_name static.example.com;
    root /var/www/static;
    
    # Enable Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
    gzip_min_length 256;
    gzip_comp_level 5;
    gzip_vary on;
    
    # Browser caching
    location ~* \.(jpg|jpeg|png|gif|ico|webp|svg)$ {
        expires 365d;
        add_header Cache-Control "public, immutable";
    }
    
    location ~* \.(css|js)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    location ~* \.(html|json)$ {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }
}

Setting different cache lifetimes by type is one of the highest-ROI tweaks: hashed CSS/JS can safely use immutable for a year, while HTML is cached for just an hour so content updates go live quickly.

A common cache strategy by resource type:

Resource Suggested Cache Time Cache-Control Notes
Hashed CSS/JS 365 days public, immutable URL changes with filename, safe to cache long
Images/fonts 30-365 days public, max-age=... Adjust by update frequency
HTML pages 5 min - 1 hour must-revalidate Keep content updates timely
API responses As needed no-store or short TTL Avoid serving stale data

Reverse proxy config

server {
    listen 443 ssl http2;
    server_name example.com;
    
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;
        
        # Buffering optimization
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 32k;
        proxy_busy_buffers_size 64k;
        proxy_temp_file_write_size 64k;
    }
    
    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
    }
}

The Connection "upgrade" lines are required for WebSocket, or Nginx will kill the long-lived connection; X-Forwarded-Proto tells the backend the user arrived over HTTPS so it doesn't generate wrong URLs.

Security hardening

# Hide the Nginx version
server_tokens off;

# 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 Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

# Rate limiting
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=30r/s;
location /login/ {
    limit_req zone=mylimit burst=5 nodelay;
}

# Connection limiting
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
    limit_conn addr 100;
}

Adding limit_req to sensitive endpoints like login, signup, or APIs is the simplest effective defense against brute force and endpoint scraping. Example: after a blog set its comment endpoint to rate=5r/s, scraper-flooded comments slowed immediately and server load dropped noticeably — while a forum without rate limiting got flooded with over a hundred thousand spam posts in a few hours. Don't set limits too tight either; leave headroom for real traffic or you'll hurt legitimate users. Start around rate=10r/s, watch for collateral damage, then tighten.

These security header lines look trivial but are among the easiest things to forget before launch: X-Frame-Options blocks clickjacking via iframe embedding, X-Content-Type-Options: nosniff stops MIME sniffing, and Permissions-Policy declares browser permissions centrally. They have no functional impact, so it's worth adding them globally on every site. See also security headers.

Performance tuning tips

1. Use HTTP/2 or HTTP/3

HTTP/2 multiplexes and cuts connection counts; HTTP/3 (QUIC) lowers latency further on weak networks:

listen 443 ssl http2;
# HTTP/3 needs extra config
listen 443 quic reuseport;

2. Enable OpenSSL hardware acceleration

ssl_engine aesni;

3. Tune kernel parameters

# /etc/sysctl.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_tw_buckets = 2000000
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_tw_reuse = 1
net.core.netdev_max_backlog = 5000

Apply with sysctl -p. Kernel and Nginx settings must match — for example, after raising somaxconn, also raise the backlog on your listen directive.

Testing commands

# Test the Nginx configuration
nginx -t

# Check Nginx status
systemctl status nginx

# Real-time request stats
tail -f /var/log/nginx/access.log | grep -c "GET"

# Load test with ab
ab -n 10000 -c 100 https://example.com/

Reference: Nginx official docs https://nginx.org/en/docs/

Log management and rotation

Nginx writes access logs to a single file by default; once traffic picks up it balloons into gigabytes, eating disk and slowing writes. Use logrotate to rotate daily and keep 30 days. Linux usually ships a template — just confirm it matches:

# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    rotate 30
    compress
    missingok
    notifempty
    sharedscripts
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
    endscript
}

The kill -USR1 tells Nginx to reopen its log files so the old one stops being written after rotation.

Common problems

Symptom Cause Fix
502 Bad Gateway Backend down or crashed Check the proxy_pass target process and logs
504 Gateway Timeout Backend response too slow Raise proxy_read_timeout or optimize the backend
too many open files File-descriptor limit too low Raise worker_rlimit_nofile and the system ulimit
Connections refused at high concurrency Backlog too small Raise somaxconn and listen backlog

16IDC Takeaway

For most sites, Nginx plus a CDN already meets performance needs. Getting gzip, caching, and security headers right delivers a big experience boost without extra hardware. Quantify before tuning: record current QPS and latency with ab or wrk, change one setting, then load-test again — don't stack parameters on a hunch. A sensible order is: gzip and caching first (fastest wins), then keepalive and kernel parameters, and only finally HTTP/3 and hardware acceleration.

For a suitable Nginx hosting environment, see the server selection guide.