E-NO
Nginx performance 5 Min Read

Nginx Performance Tuning with Practical Examples: A Practical Implementation Guide

calendar_today Published: 2026-08-26
update Last Updated: 2026-08-26
analytics SEO Efficiency: 97%
Technical guide illustration for Nginx Performance Tuning with Practical Examples: A Practical Implementation Guide.

Intro

Nginx is widely deployed as a web server, reverse proxy, and load balancer because it is fast and reliable out of the box. However, the default configuration is intentionally conservative: it works on almost any hardware but leaves significant performance on the table for high-traffic or latency-sensitive workloads. Performance tuning is not about blindly raising limits; it is a systematic process of understanding your workload, establishing a quantifiable baseline, making one change at a time, and verifying the result. Without that discipline, you risk destabilizing a working system or chasing changes that look good in isolation but do not help real users.

This guide provides a practical, command-driven approach for Nginx administrators and DevOps engineers. You will learn how to inventory your environment, safely modify key settings for static file serving and proxying, run realistic load tests, and recover quickly if a change degrades performance. Every major step includes concrete commands, expected output, and interpretation guidance.

Version and Environment Inventory

Before touching a single directive, document exactly what you are running. Nginx behavior varies across versions and custom builds, especially with third-party modules, threading support, and HTTP/2 or SSL options. Run nginx -V to see the compile-time configuration. The output is long, so filter for the most relevant flags:

nginx -V 2>&1 | tr ' ' '\n' | grep -E 'nginx/|--with-threads|--with-http_v2_module|--with-http_ssl_module'

Expected output resembles:

nginx/1.24.0
--with-threads
--with-http_v2_module
--with-http_ssl_module

The presence of --with-threads means you can use thread pools for blocking operations such as disk I/O. Without it, all disk reads happen in the worker process event loop. If you see a custom OpenSSL path or third-party modules like --add-module=..., note them because they can affect TLS performance and memory usage.

Next, record the operating system kernel and system limits. Nginx performance is tightly coupled to file descriptor limits, socket buffers, and network parameters:

ulimit -n
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.ip_local_port_range

A common baseline is:

ulimit -n
1024

But for a server handling thousands of concurrent connections, you need at least 65535. We will address that later.

Also capture hardware and topology information. Use nproc to get CPU cores, free -h for memory, and lsblk or iostat to identify disk type (HDD vs. SSD). For example:

nproc
free -h | head -2
lsblk -d -o NAME,ROTA,SIZE

The ROTA field tells you if the disk is rotational (1 for HDD, 0 for SSD). That matters because static file serving performance differs dramatically between HDD and SSD, especially with sendfile and direct I/O.

Finally, define the workload type. Is Nginx:

  • Serving static files directly from disk?
  • Proxying requests to an upstream application server (e.g., Node.js, PHP-FPM, Java)?
  • Terminating TLS connections?
  • Acting as a load balancer?

Each scenario has a different optimization path. Static file serving benefits from sendfile, tcp_nopush, and aggressive caching. Proxying benefits from upstream keepalive, buffer tuning, and HTTP/1.1 connection reuse. TLS termination requires efficient session caching and possibly hardware acceleration.

Record all findings in a simple text file or wiki page. You will refer back to this inventory when deciding which directives to change and when interpreting benchmark results.

Safe Configuration Path

Changing production configuration without a safety net is reckless. Follow these rules:

  1. Back up everything first. Copy /etc/nginx/nginx.conf and any included files (often in /etc/nginx/conf.d/ or /etc/nginx/sites-enabled/) to a timestamped backup.
  2. Validate syntax before applying. Run nginx -t. It catches typos, missing semicolons, and invalid directives without affecting the running process.
  3. Test in a staging or canary environment. If possible, run a second Nginx instance on a non-production port (e.g., 8080) with the same configuration and workload simulation.
  4. Use graceful reload for production changes. nginx -s reload starts new worker processes with the new configuration and gracefully shuts down old workers after they finish existing requests. It does not drop active connections.
  5. Make one change at a time. Changing five parameters at once makes it impossible to know which one caused the improvement or regression.

Now let us walk through a concrete baseline configuration for a typical Nginx server that both serves static files and proxies dynamic requests. Start with the default nginx.conf, then apply the most broadly beneficial adjustments.

Worker Processes and Connections

The two most impactful directives are worker_processes and worker_connections.

  • worker_processes: Should be set to auto on modern Nginx (1.3.8+). This creates one worker process per CPU core, which is optimal for most workloads. If you have many CPU-bound SSL handshakes, you might set it to the number of cores minus one to leave headroom for the OS, but auto is a safe starting point.
  • worker_connections: This sets the maximum number of simultaneous connections each worker process can handle. The default is often 512 or 1024, which is too low for high concurrency. The total connections = worker_processes * worker_connections. For a server that should handle 10,000 concurrent clients, set worker_connections to 10240 or more, but make sure the system limit (ulimit -n) is at least double that (each connection uses a file descriptor).

Here is a baseline configuration snippet:

worker_processes auto;

events {
    worker_connections 4096;
}

http {
    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;

    upstream backend {
        server 10.0.0.10:8080;
        server 10.0.0.11:8080;
        keepalive 32;
    }

    server {
        listen 80;
        server_name example.com;

        location /static/ {
            root /var/www;
            expires 30d;
            access_log off;
        }

        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

Let us break down each setting:

  • worker_processes auto; uses all CPU cores.
  • worker_connections 4096; allows each worker to handle 4096 concurrent connections, for a theoretical total of cores * 4096.
  • sendfile on; enables the sendfile() system call, which copies data directly from the kernel page cache to the socket without passing through user space. This dramatically improves static file throughput and reduces CPU usage.
  • tcp_nopush on; tells Nginx to send HTTP response headers in one packet and the file data in another, reducing the number of packets and improving network efficiency. It works in conjunction with sendfile.
  • keepalive_timeout 65; keeps client connections open for 65 seconds of inactivity, allowing subsequent requests to reuse the same TCP connection. This reduces the overhead of TCP handshakes, especially for HTTPS.
  • The upstream block defines a backend group with two servers and a keepalive 32; directive. This directive instructs Nginx to maintain up to 32 idle keepalive connections to each upstream server. This is crucial for reverse proxy performance because it avoids the TCP and TLS handshake overhead on every request. Without it, Nginx opens a new connection to the backend for each request.
  • In the location / block, proxy_http_version 1.1; and proxy_set_header Connection ""; are required to enable keepalive to the upstream. Nginx defaults to HTTP/1.0 for proxying, which closes the connection after each request unless explicitly told otherwise.
  • The static location sets expires 30d; to add caching headers for browsers, and access_log off; to reduce disk I/O for static file requests.

After making these changes, validate and reload:

nginx -t
nginx -s reload

Expected output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If you see any error, fix it before proceeding.

System-Level Adjustments

Sometimes Nginx configuration is correct, but the operating system limits prevent it from using its full capacity. For high concurrency, increase the open file limit for the Nginx user. On systemd-based systems, create an override file:

mkdir -p /etc/systemd/system/nginx.service.d
printf '[Service]\nLimitNOFILE=65535\n' > /etc/systemd/system/nginx.service.d/limit.conf
systemctl daemon-reload
systemctl restart nginx

Then verify with cat /proc/$(pgrep nginx | head -1)/limits | grep 'Max open files'. The value should be 65535.

Also tune kernel network parameters to handle many connections. Add the following to /etc/sysctl.conf or a file in /etc/sysctl.d/:

net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_fin_timeout = 15
net.core.netdev_max_backlog = 65535

Then apply with sysctl -p. These settings enlarge the accept queue, SYN backlog, and local port range, which prevent connection drops under load.

Verification and Diagnostics

After applying initial tuning, you need a repeatable benchmark to measure impact. Do not rely on ab for serious testing; it is single-threaded and can be optimistic. wrk and wrk2 are modern HTTP benchmark tools that produce higher loads and report latency percentiles. Here is a realistic test command for an Nginx server serving a mix of static and dynamic content:

wrk -t4 -c100 -d30s --latency http://localhost/

Expected output snippet:

Running 30s test @ http://localhost/
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    23.45ms   12.34ms 210.50ms   78.9%
    Req/Sec     2.13k   234.56     2.89k    68.0%
  Latency Distribution
     50%   18.23ms
     75%   25.67ms
     90%   35.89ms
     99%   89.12ms
  255456 requests in 30.02s, 320.11MB read
Requests/sec:   8508.45
Transfer/sec:     10.67MB

Analyze the output:

  • Requests/sec (RPS): 8508.45. This is the primary throughput metric.
  • Latency percentiles: The p99 of 89.12ms means 1% of requests took longer than 89ms. For a low-latency API, you might want p99 under 50ms.
  • Error rate: wrk does not show errors by default, but if you see connection timeouts or non-2xx responses, that is a red flag.

Run the same benchmark against your baseline configuration (before changes) and after each incremental change. Keep the test duration and concurrency identical for comparison.

For a more controlled latency test, use wrk2 which produces a constant request rate:

wrk2 -t4 -c100 -d30s -R2000 --latency http://localhost/

This sends 2000 requests per second regardless of server capacity. If the server cannot keep up, latency will climb and you can pinpoint the breaking point.

Monitoring Nginx Internals

Enable the stub status module to see live connection and request counters. Add this to your configuration:

location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

Access it with curl http://localhost/nginx_status. Output looks like:

Active connections: 291
server accepts handled requests
 16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106

Interpretation:

  • Active connections: Total current connections, including idle keepalive.
  • accepts: Total connections accepted.
  • handled: Total connections handled (equal to accepts if no resource limits hit).
  • requests: Total requests.
  • Reading: Connections reading request headers.
  • Writing: Connections writing responses.
  • Waiting: Idle keepalive connections waiting for another request.

If handled is much lower than accepts, you are running out of worker connections. If Waiting is high, keepalive is working well.

For deeper analysis, check system resources during load:

top -b -n1 | head -20
vmstat 1 10
iostat -x 1 5
  • top shows CPU usage of Nginx workers. If each worker is at 100% CPU, you are CPU-bound and may need more efficient code paths or more cores.
  • vmstat shows memory and context switches. High context switches can indicate excessive system calls, perhaps from many small requests.
  • iostat shows disk utilization. If %util is near 100% for the disk serving static files, you may need an SSD or better caching.

Failure Modes and Recovery

Even careful tuning can introduce problems. Here are common failure modes and how to recover from each.

Worker Connection Exhaustion

Symptom: Nginx returns 500 errors or drops connections under load. nginx_status shows handled much lower than accepts. Error log contains worker_connections are not enough.

Recovery: Increase worker_connections and the system file descriptor limit (see above). For example:

events {
    worker_connections 8192;
}

Then reload. If the problem persists, check dmesg for nf_conntrack: table full or increase net.core.somaxconn.

Upstream Connection Problems

Symptom: 502 Bad Gateway errors increase. Error log says upstream prematurely closed connection while reading response header.

Cause: Often caused by proxy buffering settings that are too small, or missing keepalive configuration causing upstream servers to close idle connections.

Fix: Ensure you have proxy_http_version 1.1; and proxy_set_header Connection ""; in the proxy location. Set reasonable buffer sizes:

proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;

Also verify upstream server timeouts. If the backend is slow, increase proxy_read_timeout (default 60s is usually fine).

Memory Exhaustion from Large Buffers

Symptom: Nginx worker processes get killed by OOM killer. dmesg shows Out of memory: Kill process. Memory usage grows unexpectedly.

Cause: Overly large buffer settings or too many large responses being cached.

Fix: Reduce buffer sizes or set a limit on response buffering. For example:

proxy_buffers 4 8k;
proxy_buffer_size 2k;

Or disable buffering for large streaming responses (proxy_buffering off; for specific locations).

Rollback Procedure

Always keep a known-good configuration backup. If a change causes performance degradation or errors, restore the backup and reload:

cp /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf
nginx -t
nginx -s reload

Expected output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Then verify with curl -I http://localhost/ to confirm the server is responding normally.

If the server fails to start after a change, Nginx may abort with an error. In that case, the old master process is still running, but you can also start a separate instance with a minimal configuration to restore service:

nginx -c /etc/nginx/nginx.fallback.conf

Document every change in a log file: what you changed, why, and the benchmark before and after. This makes it trivial to identify and revert problematic settings.

Operations Checklist

Use the following checklist for every tuning iteration. It ensures a disciplined, repeatable process.

StepActionTool/CommandExpected Result
1Record baseline metricswrk -t4 -c100 -d30s http://localhost/Record RPS and latency percentiles
2Identify bottlenecktop, vmstat, iostat, nginx_statusDetermine if CPU, I/O, or network bound
3Change one parameterEdit nginx.confMake a single, documented change
4Validate confignginx -tOutput: syntax is ok and test is successful
5Apply changenginx -s reloadNo errors, new worker processes started
6Re-run load testSame wrk command as step 1Improved or equal metrics
7If improvement, keepDocument the changeNote in change log
8If regression, rollbackcp backup && nginx -s reloadRestored previous performance
9Monitor for 24 hoursnginx_status, logs, system metricsNo anomalies, stable performance

Pro tip: Automate the baseline and post-change benchmarks with a script that saves results to a timestamped file, so you can easily compare across iterations.

Conclusion

Nginx performance tuning is not a one-time event but an iterative process that yields measurable improvements when approached systematically. By inventorying your environment, making scoped changes, verifying with concrete metrics, and having a rollback plan, you can safely optimize for your specific workload. Start with a narrow, inspectable pilot as recommended, then gradually expand based on observed results. Use the provided commands and checklist to build confidence and maintain performance over time. With the right methodology, you can unlock Nginx's full potential and keep your applications fast and reliable under even the heaviest loads.

Article Quality Score

Reader usefulness 97%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL