Nginx Performance Tuning: Complete Guide

· Priya Sharma · 10 min read

Advanced

Nginx powers over 34% of all websites and is the most popular web server for high-traffic sites, CDN edge servers, and reverse proxy deployments. Its event-driven, non-blocking architecture handles thousands of concurrent connections with minimal memory — but only when properly tuned. Default Nginx configuration is conservative and safe for any hardware. Production tuning can improve throughput by 2-10x and reduce latency by 30-50% depending on the workload.

This guide covers every major performance dimension: worker processes, connection handling, buffer tuning, compression, static file serving, SSL/TLS optimization, caching, and upstream proxy tuning. Each section includes specific directives with recommended values and explanations of why they matter.

How Does Nginx Architecture Enable High Performance?

Nginx uses an event-driven, non-blocking I/O model. A single worker process handles thousands of connections simultaneously using OS-level event notification mechanisms (epoll on Linux, kqueue on FreeBSD). This contrasts with Apache's traditional model where each connection requires a dedicated thread or process.

The key insight is that most time in HTTP request processing is spent waiting — waiting for network I/O, waiting for disk reads, waiting for upstream responses. Nginx's event loop handles all these wait states in a single thread, multiplexing across thousands of connections. This eliminates the memory and CPU overhead of thousands of threads and the context-switching cost of managing them.

Nginx event-driven architecture showing master process, worker processes, and event loop handling multiple concurrent connections Nginx Event-Driven Architecture Master Process Worker 1 Worker 2 Worker N epoll event loop 4096+ connections non-blocking I/O epoll event loop 4096+ connections non-blocking I/O epoll event loop 4096+ connections non-blocking I/O N = CPU cores (worker_processes auto)

How Should You Configure Workers and Connections?

# /etc/nginx/nginx.conf — main context

# Match CPU cores (auto is recommended)
worker_processes auto;

# Increase file descriptor limit per worker
worker_rlimit_nofile 65535;

events {
    # Max simultaneous connections per worker
    worker_connections 4096;

    # Accept multiple connections at once
    multi_accept on;

    # Use epoll on Linux (most efficient)
    use epoll;
}

worker_processes auto sets one worker per CPU core. Each worker handles up to worker_connections concurrent connections. The total concurrent connection capacity is worker_processes × worker_connections. For a 4-core server with 4096 connections per worker, that is 16,384 concurrent connections.

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages

worker_rlimit_nofile sets the maximum number of file descriptors per worker. Each connection uses one file descriptor, and each cached file uses another. Set this to at least 2 × worker_connections to ensure workers never run out of file descriptors.

How Do You Optimize Static File Serving?

http {
    # Zero-copy file transfer (kernel to socket, bypasses userspace)
    sendfile on;

    # Optimize packet sizes (send headers and beginning of file in one packet)
    tcp_nopush on;

    # Disable Nagle's algorithm (send small packets immediately)
    tcp_nodelay on;

    # Cache open file descriptors and metadata
    open_file_cache max=10000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # KeepAlive settings
    keepalive_timeout 65;
    keepalive_requests 1000;
}

sendfile on enables zero-copy file transfers. Without it, Nginx reads the file into userspace memory and then writes it to the socket. With sendfile, the kernel transfers file data directly from disk to the network socket, eliminating a memory copy and reducing CPU usage.

open_file_cache caches file descriptors, file sizes, modification times, and directory lookups. Without it, Nginx calls stat() on every request to check if a file exists and get its metadata. For servers handling thousands of requests per second for static files, this cache eliminates millions of system calls.

How Do You Configure Gzip and Brotli Compression?

Compression reduces transfer sizes by 60-90% for text-based content, dramatically improving load times — especially on slow connections.

# Gzip compression
gzip on;
gzip_comp_level 5;           # Best speed/ratio tradeoff
gzip_min_length 256;         # Don't compress tiny responses
gzip_vary on;                # Add Vary: Accept-Encoding
gzip_proxied any;            # Compress for all proxy requests
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/json
    application/javascript
    application/xml+rss
    application/atom+xml
    image/svg+xml;

# Brotli compression (requires ngx_brotli module)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css text/xml text/javascript
    application/json application/javascript application/xml+rss
    application/atom+xml image/svg+xml;

Brotli typically achieves 15-20% better compression than gzip at comparable CPU cost. However, Brotli requires HTTPS (browsers only accept br encoding over TLS). Use both: serve Brotli to clients that support it and fall back to gzip for the rest.

How Do You Tune SSL/TLS for Performance?

TLS handshakes are the most expensive part of HTTPS connections. Proper SSL configuration minimizes handshake overhead:

# SSL session caching (avoids repeated full handshakes)
ssl_session_cache shared:SSL:20m;  # 20MB ≈ 80,000 sessions
ssl_session_timeout 1d;
ssl_session_tickets off;           # Security: disable for forward secrecy

# OCSP stapling (avoids client OCSP lookups)
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

# TLS 1.2 and 1.3 only
ssl_protocols TLSv1.2 TLSv1.3;

# Modern cipher suites
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_prefer_server_ciphers off;

# Buffer size (4k for low latency, 16k default for throughput)
ssl_buffer_size 4k;

The ssl_session_cache shared:SSL:20m directive stores TLS session parameters in shared memory, allowing session resumption without a full handshake. A 20MB cache holds approximately 80,000 sessions. This reduces TLS handshake time from ~30ms to ~5ms for returning clients.

How Do You Set Up Nginx Proxy Caching?

Nginx can cache upstream responses, serving them directly without contacting the backend — similar to a CDN but at the origin level.

# Define cache zone in http context
proxy_cache_path /var/cache/nginx
    levels=1:2
    keys_zone=app_cache:10m
    max_size=2g
    inactive=60m
    use_temp_path=off;

server {
    location /api {
        proxy_pass http://backend;
        proxy_cache app_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout http_502 http_503 http_504;
        proxy_cache_lock on;           # Prevent cache stampede
        proxy_cache_background_update on; # Refresh in background

        # Add cache status header for debugging
        add_header X-Cache-Status $upstream_cache_status;
    }
}

proxy_cache_lock on prevents cache stampedes by allowing only one request to pass through to the backend when the cache is empty. Other requests for the same resource wait for the first response to be cached.

proxy_cache_use_stale serves stale cached content when the backend returns errors or times out — the same concept as stale-while-revalidate in CDNs.

How Do You Benchmark Nginx Performance?

After tuning, measure the impact with these benchmarking tools:

# wrk (recommended for HTTP benchmarks)
wrk -t4 -c400 -d30s https://example.com/

# Apache Bench (simple, quick tests)
ab -n 10000 -c 100 https://example.com/

# Monitor Nginx built-in status
location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

The stub_status module provides real-time metrics: active connections, accepted connections, handled connections, and request count. Monitor these during load tests to identify when Nginx hits its limits.

Nginx optimization priority list showing which tuning areas provide the highest performance impact Nginx Optimization Impact (Highest → Lowest) Proxy Caching (10-100x reduction in backend requests) Gzip/Brotli Compression (60-90% smaller transfers) SSL Session Cache (5x faster TLS resumption) sendfile + open_file_cache

External References

References & Standards: HTTP/2 specification · OWASP CDN Security