Updated October 2026: Fixed invalid or outdated directives (tcp_fastopen, keepalive_requests 100, listen ... http2), switched TLS to Mozilla’s current intermediate profile, and added HTTP/3, real static caching headers and upstream keepalive.

I. Introduction

Nginx is renowned for its efficiency and versatility in serving web content. However, optimizing its configuration is crucial to ensure optimal performance. This log outlines key techniques to optimize Nginx configuration for enhanced performance.

After every change, test the configuration first and only then reload. A reload starts new workers with the new configuration and gracefully retires the old ones, so it does not drop connections the way a restart does:

sudo nginx -t && sudo systemctl reload nginx

II. Enabling Keepalive Connections

Keepalive connections allow persistent connections between clients and the server, reducing connection establishment overhead.

Example:

http {
    keepalive_timeout 65s;       # default is 75s
    # keepalive_requests 1000;   # already the default since nginx 1.19.10
}

Older guides often set keepalive_requests 100;. That was the default before nginx 1.19.10; on any current version it lowers the limit (the default is now 1000), so leave it out unless you really need a different value.

III. Implementing Gzip Compression

Gzip compression reduces the size of data transmitted between the server and clients, enhancing page load times and conserving bandwidth.

Example:

http {
    gzip on;
    gzip_vary on;          # adds "Vary: Accept-Encoding" so caches keep both versions
    gzip_proxied any;      # also compress responses to requests that came through a proxy/CDN
    gzip_comp_level 5;
    gzip_min_length 256;
    gzip_types text/plain text/css text/xml text/javascript
               application/javascript application/json application/xml
               application/rss+xml application/atom+xml image/svg+xml;
}

A few notes:

  • text/html is always compressed once gzip on; is set, so it does not need to be listed in gzip_types.
  • JavaScript is served as application/javascript by the mime.types file that ships with nginx, while text/javascript is the type current standards recommend, so list both. The legacy application/x-javascript is not needed.
  • gzip_proxied decides whether responses to proxied requests (detected by the Via request header) are compressed; the default off skips them.
  • Brotli usually compresses text better than gzip, but it is not part of nginx itself. It is available through the third-party ngx_brotli module (brotli on;, brotli_types ...;), which must be compiled in or loaded as a dynamic module.

IV. Caching Static Assets

Caching static assets minimizes the need to fetch them from the origin server repeatedly, improving response times and reducing server load. There are two separate layers here: telling browsers and CDNs how long they may keep a file, and letting nginx itself cache file metadata or proxied responses.

1. Browser and CDN caching with expires / Cache-Control

Example:

server {
    # Fingerprinted build output (e.g. app.3f9a1c.js): safe to cache "forever"
    location /assets/ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    # Other images and fonts: cache for a week
    location ~* \.(?:png|jpe?g|gif|webp|avif|svg|ico|woff2)$ {
        expires 7d;   # sets Expires and Cache-Control: max-age=604800
    }
}

Only use immutable and a one-year max-age for files whose name changes when their content changes; otherwise users keep a stale copy.

Watch out for add_header inheritance: a block inherits add_header directives from its parent only if it defines none of its own. Adding a Cache-Control header in a location therefore silently drops any security headers (HSTS, Alt-Svc, …) set at the server level, unless you repeat them or, on nginx 1.29.3+, use add_header_inherit merge;.

2. Caching open file descriptors with open_file_cache

When nginx serves many small static files, open_file_cache keeps file descriptors, sizes and modification times in memory instead of looking them up on every request:

http {
    open_file_cache max=10000 inactive=60s;
    open_file_cache_valid 60s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;
}

3. Caching proxied responses with proxy_cache

proxy_cache_path on its own only declares a cache zone; nothing is cached until a location uses it with proxy_cache:

http {
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        location / {
            proxy_cache my_cache;
            proxy_cache_valid 200 301 302 10m;
            proxy_cache_valid 404 1m;
            proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
            proxy_cache_lock on;
            add_header X-Cache-Status $upstream_cache_status;

            proxy_pass http://backend;
        }
    }
}

proxy_cache_use_stale ... updating serves the cached copy while a fresh one is fetched, and proxy_cache_lock makes concurrent misses for the same key wait for a single upstream request. X-Cache-Status (HIT, MISS, EXPIRED, …) is handy for debugging.

V. Optimizing TLS/SSL Configuration

Optimizing TLS/SSL configuration involves specifying modern protocols and strong ciphers for improved security and performance. Rather than hand-picking a cipher string, follow Mozilla’s “intermediate” profile (now generated by the TLSRef Configurator, formerly ssl-config.mozilla.org), which is the recommended setting for general-purpose servers.

Example (Mozilla intermediate, guidelines v6.0):

http {
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        http2 on;

        server_name www.example.com;
        ssl_certificate     /etc/ssl/example.com/fullchain.pem;
        ssl_certificate_key /etc/ssl/example.com/privkey.pem;

        # HSTS (63072000 seconds = 2 years)
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    }

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;
    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;

    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
}

What changed compared to older configs:

  • ssl_prefer_server_ciphers off: every cipher in the list is strong, so the client is allowed to pick the one that is fastest on its hardware (for example ChaCha20 on phones without AES acceleration).
  • ssl_ciphers only governs TLS 1.2. TLS 1.3 cipher suites are configured separately (via ssl_conf_command Ciphersuites ...) and the OpenSSL defaults are fine.
  • ssl_ecdh_curve X25519MLKEM768:... enables the post-quantum hybrid key exchange first. X25519MLKEM768 requires OpenSSL 3.5 or newer; on older OpenSSL, drop it from the list or nginx will refuse the configuration.
  • ssl_session_cache / ssl_session_timeout let returning clients resume a session instead of doing a full handshake, which saves a round trip and CPU.
  • http2 on; is the way to enable HTTP/2 since nginx 1.25.1. The old listen 443 ssl http2; parameter still works but is deprecated.

Optional: HTTP/3 (QUIC)

nginx 1.25.0+ can also serve HTTP/3 over QUIC (UDP). The ngx_http_v3_module is still marked experimental and is not built by default, so check that nginx -V shows --with-http_v3_module (the nginx.org Linux packages include it). Add QUIC listeners on the same port as HTTPS and advertise them with Alt-Svc:

server {
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    # Tells browsers HTTP/3 is available on UDP 443 for the next 24 hours
    add_header Alt-Svc 'h3=":443"; ma=86400';

    # ... certificates and the TLS settings above
}

Socket options such as reuseport (and fastopen below) may appear only once per address:port pair, so with several server blocks put them on one listen only. Remember to open UDP 443 in your firewall.

VI. Fine-Tuning Worker Processes

Adjusting the number of worker processes and connections optimizes resource utilization and concurrent request handling.

Example:

worker_processes auto;          # one worker per CPU core
worker_rlimit_nofile 65535;     # raise the open-file limit for workers

events {
    worker_connections 4096;
}

worker_connections counts all connections, including the ones nginx opens to proxied servers, so a reverse proxy uses roughly two per client request. The real limit is also capped by the number of open files, which is why worker_rlimit_nofile is raised alongside it.

VII. TCP Optimization

TCP optimization involves settings like sendfile, TCP_NOPUSH/TCP_CORK, TCP_NODELAY and TCP Fast Open to minimize latency and improve connection performance.

Example:

http {
    sendfile on;      # kernel copies files directly to the socket (default is off)
    tcp_nopush on;    # only takes effect together with sendfile
    tcp_nodelay on;   # already the default

    server {
        # TCP Fast Open: a parameter of `listen`, not a standalone directive
        listen 443 ssl fastopen=256;
    }
}
  • tcp_nopush lets nginx send the response headers and the start of a file in one packet and then send full packets, but it is applied only when sendfile is on.
  • There is no tcp_fastopen directive in nginx; writing one makes nginx -t fail. TCP Fast Open is enabled per listening socket with listen ... fastopen=N, where N limits the queue of connections that have not finished the handshake. On Linux the kernel must also allow server-side TFO (sysctl -w net.ipv4.tcp_fastopen=3, which enables both client and server support). Only enable it if your application can safely handle the same SYN data arriving more than once.

VIII. Minimizing Redirects

Reducing the number of redirects improves page load times and enhances user experience. Every redirect is an extra round trip (and, for a new host, an extra DNS lookup and TLS handshake) before the browser can even start loading the page.

A common anti-pattern is a redirect chain: http://example.com → https://example.com → https://www.example.com. Instead, send every non-canonical variant straight to the final URL in a single hop:

Example:

# http://example.com and http://www.example.com -> https://www.example.com (one hop)
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://www.example.com$request_uri;
}

# https://example.com -> https://www.example.com
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;
    # ssl_certificate ... (must cover example.com)
    return 301 https://www.example.com$request_uri;
}

return is cheaper and clearer than rewrite for this, and $request_uri keeps the original path and query string. Also make sure your own links, sitemaps and canonical tags point at the final URL so users do not hit the redirect at all.

IX. Utilizing Load Balancing

Load balancing distributes traffic across multiple backend servers, improving scalability and reliability. To avoid opening a new TCP connection to the backend for every request, keep a pool of idle upstream connections with keepalive.

Example:

http {
    upstream backend {
        server backend1.example.com;
        server backend2.example.com;

        keepalive 32;   # idle connections kept open per worker
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
        }
    }
}

Upstream keepalive needs HTTP/1.1 towards the backend and an empty Connection header (nginx otherwise sends Connection: close). Before nginx 1.29.7 both lines above are required, because the default was HTTP/1.0 with no keepalive. Since 1.29.7, proxy_http_version 1.1 is the default and upstream keepalive is enabled by default with 32 connections per worker, so the extra lines are harmless but no longer necessary.

X. Logging Optimization

Optimizing logging involves configuring access and error logging to balance information capture and performance impact.

Example:

http {
    access_log /var/log/nginx/access.log combined buffer=16k flush=5s;
    error_log /var/log/nginx/error.log warn;
}

buffer=16k batches log writes instead of writing on every request. Without flush, buffered lines are only written when the buffer fills or the worker reopens its logs, so on a quiet server they can sit in memory for a long time; flush=5s caps that delay. You can also turn off access logging entirely for noisy static paths (access_log off;).

XI. Conclusion

Optimizing Nginx configuration is essential for achieving peak performance in web applications. By implementing these optimization techniques, developers can enhance server efficiency, improve user experience, and ensure scalability for their applications. Always validate with nginx -t, reload rather than restart, and measure before and after each change.