2026年11月12日

2026年11月12日

Nginx Performance Tuning: Measure First, Then Change What Works

This article is a localized edition of the Japanese original. Read the Japanese original

The Nginx settings most likely to pay off are the number of workers and the connection limit, keepalive, compression, how static files are sent, and buffer sizes. But rather than pasting in "big values you saw online," it is safer to measure before you change anything, change one item at a time and confirm the difference, and that also leaves you with a record of the results. This article walks through what the main settings mean, how to choose values, common pitfalls, and how to verify (it assumes the stable version of Nginx as of October 2026).

Start by setting up measurement

Begin tuning only once you know where things are slow. If the CPU has headroom but responses are slow, the cause is often PHP-FPM or the database, and changing Nginx settings will not help. On the other hand, if static file delivery or concurrent connections are the bottleneck, Nginx settings do make a difference.

Logging the processing time in the access log lets you separate Nginx's own time from the time spent upstream (PHP-FPM or the application).

http {
    log_format timed '$remote_addr [$time_local] "$request" $status '
                     '$body_bytes_sent rt=$request_time urt=$upstream_response_time';
    access_log /var/log/nginx/access.log timed;
}

If $request_time is large and $upstream_response_time is about the same, the upstream is what is slow. If $request_time is large for static files, also suspect the network, the disk, or a slow client.

To compare under load, use wrk or ab (the apache2-utils package on Debian/Ubuntu). Avoid putting heavy load on a production server from outside; test on a staging environment, or with light load during quiet hours.

# Hit the home page for 30 seconds with 4 threads and 100 connections
wrk -t4 -c100 -d30s https://example.com/

# Compare a single static file
wrk -t4 -c100 -d30s https://example.com/wp-includes/css/dist/block-library/style.min.css

Each time you change a setting, run the same command under the same conditions and record the requests per second and the latency distribution. As a rule, change one item at a time.

worker_processes and worker_connections

Nginx runs as one master process and several worker processes, and each worker handles many connections in an event-driven way.

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
}

worker_processes auto starts as many workers as there are CPU cores. Going beyond the core count does not make things faster, so auto is usually all you need.

worker_connections is the maximum number of connections one worker can hold at once. The compile-time default is 512, but the nginx.conf shipped with distribution packages often sets 768 or 1024. Keep in mind that when proxying to a reverse-proxy backend or PHP-FPM, each request uses two connections, one on the client side and one on the upstream side. Do not simply assume that "workers x worker_connections = number of clients you can serve at once."

If you raise the connection count, raise the file descriptor limit to match. worker_rlimit_nofile raises the limit for workers, but if Nginx is started by systemd, the unit's LimitNOFILE also applies. You can check what is actually in effect like this.

cat /proc/$(pgrep -f "nginx: worker" | head -n1)/limits | grep "open files"

If worker_connections are not enough does not appear in the log, there is no need to force the value up. multi_accept on and use epoll are also often recommended, but on Linux epoll is selected automatically, so writing it changes nothing, and the effect of multi_accept depends on the environment. If you add them, do so only after measuring the difference.

keepalive settings

keepalive handles multiple requests over a single TCP (and TLS) connection. Because it reduces TLS handshakes, it noticeably improves perceived speed.

http {
    keepalive_timeout 30s;
    keepalive_requests 1000;
}

The default keepalive_timeout is 75 seconds. If it is too long, idle connections tie up worker_connections. Busy sites sometimes shorten it to around 15 to 30 seconds, but too short means more reconnections, so decide while watching how the connection count changes. Since 1.19.10, the default for keepalive_requests is 1000.

Keepalive to the upstream is easy to overlook. In a reverse-proxy setup, reconnecting between Nginx and the backend on every request adds overhead.

upstream app {
    server 127.0.0.1:8080;
    keepalive 32;
}

server {
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

The keepalive in upstream is the maximum number of idle connections each worker keeps. Without proxy_http_version 1.1 and Connection "", connections are not reused. Configurations that are missing this and therefore have no effect are a common sight.

sendfile and tcp_nopush

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
}

sendfile on sends file contents to the socket inside the kernel without copying them into user space. This lowers CPU load when serving static files. tcp_nopush only works in combination with sendfile and sends the response headers and the beginning of the file in the same packet. tcp_nodelay is on by default.

One caveat: on some file systems, such as VirtualBox shared folders and NFS, sendfile has been reported to cause problems such as serving stale content. If a CSS change does not show up in your development environment, suspect sendfile.

Compression with gzip and brotli

Compressing text responses greatly reduces the amount of data transferred.

http {
    gzip on;
    gzip_comp_level 5;
    gzip_min_length 1024;
    gzip_vary on;
    gzip_proxied any;
    gzip_types text/plain text/css application/javascript application/json
               application/xml image/svg+xml;
}

text/html is always compressed even if you do not list it in gzip_types. Do not include images (JPEG, PNG, WebP) or woff2 fonts, because they are already compressed. gzip_comp_level ranges from 1 to 9; higher levels use more CPU for diminishing size reductions. Around 4 to 6 is often cited as a good compromise, but go lower on servers without spare CPU. gzip_proxied any returns compressed responses to requests that come through proxies such as a CDN as well.

brotli can achieve better compression than gzip, but it is not included in Nginx itself. Build in the ngx_brotli module published by Google, or use a dynamic module package provided by your distribution. Here is an example configuration once it is loaded.

# At the top of nginx.conf (the module path varies by environment)
load_module modules/ngx_http_brotli_filter_module.so;

http {
    brotli on;
    brotli_comp_level 5;
    brotli_types text/plain text/css application/javascript application/json
                 application/xml image/svg+xml;
}

To check that it works, send Accept-Encoding and look at the headers.

curl -sI -H "Accept-Encoding: br, gzip" https://example.com/ | grep -i content-encoding

open_file_cache

On sites with many static files, this reduces the existence checks and metadata lookups performed every time a file is opened.

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

What gets cached is information such as file descriptors, sizes and modification times, not the file contents. Because the information is not rechecked during open_file_cache_valid, right after a deployment Nginx can respond with an old file's size and break the page. On sites that replace files frequently, shorten valid or make a reload part of every deployment.

Buffer sizes

Buffers are not "the bigger, the faster." They are allocated per connection, so increasing them indiscriminately puts pressure on memory. The basic approach is to adjust them only after a specific symptom shows up in the error log.

http {
    client_max_body_size 64m;
    client_body_buffer_size 128k;
    large_client_header_buffers 4 16k;

    fastcgi_buffer_size 32k;
    fastcgi_buffers 16 16k;
}

The default client_max_body_size is 1 MB. If WordPress media uploads return "413 Request Entity Too Large," revisit this value (and adjust upload_max_filesize and post_max_size on the PHP side as well). Request bodies larger than client_body_buffer_size are written to a temporary file, producing the warning a client request body is buffered to a temporary file. If it happens often, raise the value a little.

If you see upstream sent too big header while reading response header from upstream, an application that returns cookies or long headers is running out of fastcgi_buffer_size (proxy_buffer_size when proxying). This is the area used to receive headers, so raise it.

Applying and verifying changes

After changing the configuration, check the syntax and then reload. A reload switches to workers running the new configuration without dropping existing connections.

sudo nginx -t && sudo systemctl reload nginx

If you restart while nginx -t is failing, Nginx will not start and the site goes down. Copy the production configuration files before changing them so you can roll back immediately. After the change, run the same measurement as at the start and check whether the numbers improved and whether any new warnings appear in the error log.

Summary

  • Before changing anything, use $request_time and $upstream_response_time to find what is slow, and keep measuring under the same conditions with a tool such as wrk
  • Start from worker_processes auto, and when sizing worker_connections, account for the two connections used when proxying and for the file descriptor limit
  • Do not make keepalive_timeout too long. Upstream keepalive requires proxy_http_version 1.1 and Connection ""
  • sendfile and tcp_nopush help static delivery, but watch out for problems on shared folders and NFS
  • Limit compression to text types and keep comp_level moderate. brotli requires a separate module
  • Adjust open_file_cache and buffers only after seeing symptoms, and reload only after nginx -t passes

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲