How to Tune Nginx for a High-Traffic Website
In this article11 sections
Nginx is already fast out of the box. Most high-traffic slowdowns come from a few settings that limit connections, a missing cache, or a backend that is asked for the same page thousands of times. This guide covers those settings in the order you should change them, with the command to check each one and the mistake to avoid. Test every change on a copy of the site first, and keep the old configuration so you can roll back.
Measure before you change anything
Tuning without a baseline makes it impossible to tell whether a change helped. Record three things first:
- The live configuration as Nginx reads it. This shows included files and the effective values:
sudo nginx -T | less - Active connections and request counts. Enable the status page if it is not already on. It must be reachable only from your own address:
location = /nginx_status { stub_status; allow 127.0.0.1; deny all; }Then check it with
curl http://127.0.0.1/nginx_status. - Errors under load. Watch the error log while you test:
sudo tail -f /var/log/nginx/error.log
If the error log shows worker_connections are not enough, or Too many open files, the limits below are the first things to fix.
Workers and connection limits
Nginx runs a fixed number of worker processes, and each worker handles a fixed number of connections. The configuration is in the main and events blocks:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
Two rules keep this correct:
- Maximum clients. Each worker can hold
worker_connectionsconnections. When Nginx acts as a reverse proxy, each proxied request uses two connections, one from the client and one to the backend. So the practical limit is roughlyworker_processes × worker_connections ÷ 2for proxied traffic. - File descriptors. Every connection uses a file descriptor.
worker_rlimit_nofilemust be at leastworker_connections, and the operating system limit must allow it too. Check the system limit for the Nginx service:systemctl show nginx -p LimitNOFILEIf it is lower than you need, add a drop-in and reload systemd:
sudo systemctl edit nginx[Service] LimitNOFILE=65535sudo systemctl daemon-reload && sudo systemctl restart nginx
Use restart for this one change, because the file limit only applies when the service starts. Use reload for the rest.
Static files and the socket layer
These settings let the kernel send files and responses with less copying:
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30s;
keepalive_requests 1000;
}
sendfile hands static files to the kernel. tcp_nopush sends full packets, and tcp_nodelay sends small responses without waiting. Together they are the standard pair for static content. Keep keepalive_timeout modest: a long value holds idle connections open and uses up the connection limit.
The listen socket also has a backlog, which is the queue of connections waiting to be accepted. The default is often too small for a traffic spike:
server {
listen 80 backlog=4096 reuseport;
listen 443 ssl backlog=4096 reuseport;
}
The backlog is capped by the kernel setting net.core.somaxconn. Check it with sysctl net.core.somaxconn. If it is below 4096, raise it in /etc/sysctl.d/99-nginx.conf and apply it with sudo sysctl --system.
Compression
Compress text responses. Images and videos are already compressed, so leave them out:
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_types text/css application/javascript application/json application/xml image/svg+xml;
Levels above 5 use much more CPU for a small size gain, which is rarely worth it on a busy server. Brotli usually compresses better, but it needs a separate module, so use gzip unless you have installed Brotli.
Upstream connections to the application
When Nginx proxies to an application server, reuse the connection to the backend instead of opening a new one per request:
upstream app {
server 127.0.0.1:3000;
keepalive 32;
}
server {
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 60s;
}
}
Without proxy_http_version 1.1 and the cleared Connection header, the keepalive setting does nothing. Set keepalive to a number your backend can serve at once, not the largest number you can type.
Caching the responses that repeat
The biggest gain on a busy site comes from not asking the backend at all for a page that has not changed. A cache zone and a few directives do it:
proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app:50m max_size=1g inactive=60m use_temp_path=off;
server {
location / {
proxy_cache app;
proxy_cache_valid 200 301 10m;
proxy_cache_lock on;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
add_header X-Cache-Status $upstream_cache_status always;
proxy_pass http://app;
}
}
proxy_cache_lock stops many simultaneous misses for the same page from reaching the backend at once. proxy_cache_use_stale keeps serving the last good copy while the backend is down or busy. The X-Cache-Status header tells you whether each response was a HIT, MISS or BYPASS, which is how you confirm the cache works.
Warning. Never cache pages that depend on the visitor: logged-in areas, carts and checkouts. Bypass the cache when a session cookie is present, and check that the cache key does not include private data. A cached private page is served to the next visitor.
Limiting abuse without blocking real visitors
Rate limiting protects the backend from one client sending too many requests. Set the limit above what a real person uses, and test it on your own pages first:
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=perconn:10m;
server {
location / {
limit_req zone=perip burst=20 nodelay;
limit_conn perconn 20;
}
}
Behind a CDN or load balancer, $binary_remote_addr is the proxy’s address, so every visitor looks the same. Use the real client address from the trusted proxy header instead, configured with the set_real_ip_from directive. Otherwise one bad visitor limits everyone.
TLS without the handshake cost
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
The session cache lets returning visitors skip most of the handshake. OCSP stapling sends the certificate status with the response, so the browser does not check it separately. Stapling needs the resolver line, so Nginx can fetch the certificate status. Without it, stapling is silently skipped. Confirm stapling with openssl s_client -connect example.com:443 -status.
Test the change under load
A configuration that passes nginx -t is not proof of speed. Measure the same test before and after, against a copy of the site, not the live server. A load tool such as wrk is simple to use:
wrk -t4 -c200 -d30s https://staging.example.com/
Run it with the same settings each time, and watch the error log and /nginx_status during the run. Keep the test short and gentle on a shared server. Do not test a live site you do not own, and do not run load tests that could be mistaken for an attack.
Apply the changes with a configuration test first, then a reload:
sudo nginx -t && sudo systemctl reload nginx
Common mistakes
- Raising
worker_connectionswithout raising the file limit. The worker runs out of file descriptors before it runs out of connections. - Caching by URL alone for a site with logins. One user’s page is served to another.
- Setting every limit to the maximum. Large limits hide the real bottleneck and leave the server with no protection.
- Tuning the web server while the database is the slow part. Check the backend’s response time in the log first, with
$upstream_response_timein the access log format. - Restarting instead of reloading. A reload applies configuration without dropping connections. Use a restart only when a setting needs it, such as the file limit.
When the upstream application is the bottleneck, the proxy returns errors instead of pages. If clients see a 502 or 504 after you raise these limits, read how to fix 502 Bad Gateway on Nginx and how to fix 504 Gateway Timeout on Nginx. For uploads that a busy site rejects, see how to fix 413 Request Entity Too Large on Nginx and PHP.
Frequently asked questions
How many worker processes should Nginx run?
worker_processes auto; sets one worker per CPU core, which is the right default. More workers than cores adds context switching without helping.
Is Nginx fast enough without caching?
For static files, yes. For dynamic pages, the application is usually the limit. A cache helps most when many visitors request the same pages.
What is the difference between reload and restart?
A reload reads the new configuration and keeps existing connections open. A restart stops and starts the service, which drops connections. Settings that apply only at startup, such as the file limit, need a restart.
Should I use HTTP/2 or HTTP/3?
HTTP/2 is enabled with http2 on; in recent Nginx versions, and it is worth using with TLS. HTTP/3 needs a build with QUIC support, so check that your package includes it before you rely on it.
Tune Nginx in this order: measure, raise the file and connection limits, enable the static and socket settings, add compression, reuse backend connections, cache the responses that repeat, then limit abuse. Test each change against a copy of the site, and keep the previous configuration to roll back.