Nginx Performance Tuning: The Complete Guide for 2026
A default Nginx installation handles impressive traffic out of the box, but the difference between default settings and a properly tuned configuration can be 5-10x in throughput, 50% lower latency, and dramatically reduced memory usage under load. Whether you are serving static files, proxying to application servers, or running a high-traffic API gateway, tuning Nginx is one of the highest-ROI optimizations you can make.
This guide covers every major tuning lever in Nginx with real-world configuration blocks you can drop into production. Each section explains why a setting matters, not just what to set it to, so you can adapt the recommendations to your specific workload.
Table of Contents
- Worker Processes and Connections
- Buffer Sizes and Timeouts
- Gzip Compression
- Static File Caching
- Keepalive Connections
- SSL/TLS Optimization
- Rate Limiting
- Open File Cache
- Upstream Optimization
- Monitoring and Benchmarking
- Common Mistakes and Anti-Patterns
- Real-World Tuning Checklist
- Frequently Asked Questions
1. Worker Processes and Connections
Worker processes are Nginx's execution units. Each worker runs a single-threaded event loop that handles thousands of connections concurrently. Getting the worker count and connection limits right is the foundation of all other tuning.
# /etc/nginx/nginx.conf - top level
# Set to the number of CPU cores. 'auto' detects this for you.
worker_processes auto;
# Increase the file descriptor limit per worker.
# Must be >= worker_connections * 2 (each connection uses a fd,
# and proxied connections use two: client + upstream).
worker_rlimit_nofile 65535;
# Pin workers to CPU cores to reduce context switching (optional).
# On a 4-core server: 0001 0010 0100 1000
worker_cpu_affinity auto;
events {
# Max simultaneous connections per worker.
# Total capacity = worker_processes x worker_connections.
worker_connections 4096;
# Accept as many connections as possible at once
# instead of one at a time.
multi_accept on;
# Use epoll on Linux for optimal event notification.
use epoll;
}
How to size worker_connections: Check your peak concurrent connections with stub_status. For a reverse proxy, each client request opens two connections (client-to-nginx and nginx-to-upstream), so you need roughly 2x your concurrent clients. On a 4-core server with 4096 connections per worker, your theoretical max is 16,384 simultaneous connections — more than enough for most workloads.
Also ensure your OS allows enough file descriptors. Set fs.file-max in /etc/sysctl.conf and update /etc/security/limits.conf:
# /etc/sysctl.conf
fs.file-max = 200000
net.core.somaxconn = 65535
# /etc/security/limits.conf
www-data soft nofile 65535
www-data hard nofile 65535
2. Buffer Sizes and Timeouts
Buffers control how much data Nginx holds in memory before writing to disk or forwarding to clients. Too small and Nginx spills to temporary files (slow). Too large and you waste memory that could serve more connections.
http {
# Client request body buffer. Requests smaller than this
# are held in memory; larger ones go to a temp file.
client_body_buffer_size 16k;
# Buffer for reading the client request header.
client_header_buffer_size 1k;
# For large headers (big cookies, long URLs).
# 4 buffers of 16k each.
large_client_header_buffers 4 16k;
# Maximum allowed request body size (upload limit).
client_max_body_size 50m;
# Proxy buffers: how Nginx reads responses from upstream.
proxy_buffer_size 8k; # Buffer for the response header
proxy_buffers 8 16k; # 8 buffers of 16k for the body
proxy_busy_buffers_size 32k; # Max size sent to client while
# still reading from upstream
# Timeouts - drop slow/idle connections to free resources.
client_body_timeout 12s; # Max time between body read ops
client_header_timeout 12s; # Max time to receive full header
send_timeout 10s; # Max time between write ops to client
# Proxy timeouts
proxy_connect_timeout 5s; # Time to establish upstream connection
proxy_read_timeout 30s; # Time between reads from upstream
proxy_send_timeout 10s; # Time between writes to upstream
}
Key insight: The defaults are conservative. If your API returns large JSON payloads, increase proxy_buffers so Nginx can buffer the entire response and free the upstream connection quickly. If clients upload files, increase client_body_buffer_size and client_max_body_size. For microservices with small payloads, the defaults are usually fine.
3. Gzip Compression
Gzip can reduce response sizes by 70-90% for text-based content, dramatically reducing bandwidth and improving page load times. The tradeoff is CPU usage per request.
http {
gzip on;
# Compression level: 1 (fastest) to 9 (smallest).
# 5 is the sweet spot - 90% of max compression at ~25% of max CPU.
gzip_comp_level 5;
# Minimum response size to compress (skip tiny responses).
gzip_min_length 256;
# Compress for all proxied requests.
gzip_proxied any;
# Tell caches to store separate compressed/uncompressed versions.
gzip_vary on;
# MIME types to compress. Don't compress images/videos
# (already compressed formats).
gzip_types
text/plain
text/css
text/javascript
text/xml
application/json
application/javascript
application/x-javascript
application/xml
application/xml+rss
application/atom+xml
application/vnd.ms-fontobject
font/opentype
font/ttf
image/svg+xml;
# Disable gzip for old IE versions that mangle it.
gzip_disable "msie6";
# Number and size of gzip buffers.
gzip_buffers 16 8k;
}
Pre-compression for static files: For maximum performance, pre-compress your static assets at build time and serve them with the gzip_static module. This eliminates per-request compression CPU entirely:
# Pre-compress at build time:
# gzip -k -9 /var/www/site/css/*.css
# gzip -k -9 /var/www/site/js/*.js
# Serve pre-compressed files when available
location /css/ {
gzip_static on;
expires 1y;
}
location /js/ {
gzip_static on;
expires 1y;
}
4. Static File Caching
Proper cache headers prevent browsers from re-downloading assets they already have. This is free performance — fewer requests, lower bandwidth, faster page loads.
server {
listen 443 ssl http2;
server_name example.com;
root /var/www/site;
# Hashed/versioned assets: cache forever.
# Files like app.a1b2c3.js can be cached permanently
# because the filename changes when content changes.
location ~* \.(js|css)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Cache-Status "static";
}
# Images and fonts: cache for 30 days.
location ~* \.(png|jpg|jpeg|gif|webp|avif|ico|svg|woff2?|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public";
# Disable access logging for static assets.
access_log off;
}
# HTML files: don't cache (or cache briefly).
# HTML is your entry point; must always be fresh.
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
# Enable ETag for conditional requests.
etag on;
# sendfile: kernel-level file transfer (skip user space copy).
sendfile on;
# Coalesce sendfile chunks into full TCP packets.
tcp_nopush on;
# Disable Nagle's algorithm on keepalive connections.
tcp_nodelay on;
}
The combination of sendfile, tcp_nopush, and tcp_nodelay is the "holy trinity" of static file performance in Nginx. sendfile uses the kernel's zero-copy mechanism, tcp_nopush batches the response header with the file data into full packets, and tcp_nodelay ensures the last partial packet is sent immediately.
5. Keepalive Connections
Every new TCP connection requires a three-way handshake (and TLS handshake for HTTPS). Keepalive reuses existing connections, eliminating this overhead for subsequent requests.
http {
# Client-side keepalive: how long to hold idle connections.
keepalive_timeout 65s;
# Max requests per keepalive connection before forcing a new one.
# Higher values improve performance for clients making many requests.
keepalive_requests 1000;
# Upstream keepalive: reuse connections to your backend.
# THIS IS THE BIGGEST WIN for reverse proxy setups.
upstream app_backend {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
# Keep 32 idle connections per worker in the pool.
keepalive 32;
# Max requests per upstream keepalive connection.
keepalive_requests 1000;
# Idle timeout for upstream keepalive connections.
keepalive_timeout 60s;
}
server {
location / {
proxy_pass http://app_backend;
# REQUIRED for upstream keepalive to work.
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
Critical detail: Upstream keepalive requires proxy_http_version 1.1 and proxy_set_header Connection "". Without these, Nginx sends Connection: close to the backend, defeating keepalive entirely. Enabling upstream keepalive typically improves proxy throughput by 20-40% because you eliminate the TCP and TLS handshake overhead for every proxied request.
6. SSL/TLS Optimization
TLS handshakes are the most expensive part of serving HTTPS. Session caching, OCSP stapling, and protocol selection can cut handshake time dramatically.
http {
# SSL session cache shared across all workers.
# 1 MB stores ~4000 sessions. 10m = 10 MB = ~40,000 sessions.
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# Disable session tickets unless you rotate keys.
# Stale ticket keys break forward secrecy.
ssl_session_tickets off;
# Modern protocol stack: TLSv1.2 + TLSv1.3 only.
ssl_protocols TLSv1.2 TLSv1.3;
# Let the server choose the cipher (faster, more secure).
ssl_prefer_server_ciphers on;
# Strong cipher suite. TLSv1.3 ciphers are selected automatically.
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# OCSP stapling: Nginx fetches the OCSP response and sends it
# during the handshake, saving clients 100-300ms per new connection.
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# HSTS: tell browsers to always use HTTPS.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Use ECDSA certificates for faster handshakes (vs RSA).
# ECDSA P-256 signing is ~10x faster than RSA-2048.
}
}
Quick wins: SSL session cache alone eliminates full handshakes for returning visitors. OCSP stapling removes the 100-300ms penalty of clients checking certificate revocation status. Using ECDSA certificates instead of RSA makes the signing operation roughly 10x faster, which matters at scale. HTTP/2 multiplexes many requests over a single connection, eliminating the head-of-line blocking that forced HTTP/1.1 deployments to use multiple connections.
7. Rate Limiting
Rate limiting protects your backend from abuse, brute-force attacks, and sudden traffic spikes. Nginx's limit_req module uses a leaky bucket algorithm for smooth, predictable throttling.
http {
# Define rate limit zones in the http block.
# $binary_remote_addr uses 4 bytes per IP (vs ~64 for $remote_addr).
# 10m zone stores ~160,000 IP addresses.
# General API rate limit: 10 requests/second per IP.
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# Login endpoint: 5 requests/minute per IP (brute force protection).
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# Per-server rate limit (total, not per-IP).
limit_req_zone $server_name zone=server_total:10m rate=1000r/s;
server {
# General API: allow bursts of 20 with no delay for the
# first 10, then throttle the remaining burst.
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://app_backend;
}
# Login: strict limiting, small burst.
location /api/login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://app_backend;
}
# Return a proper JSON error for rate-limited requests.
error_page 429 = @rate_limited;
location @rate_limited {
default_type application/json;
return 429 '{"error": "Too many requests. Try again later."}';
}
}
}
burst vs nodelay: Without burst, any request over the rate gets a 503 immediately. With burst=20, Nginx queues up to 20 excess requests and processes them at the defined rate. Adding nodelay processes burst requests immediately (without artificial delay) but still counts them against the rate, so the next requests after the burst will be throttled. For APIs, burst=20 nodelay provides the best user experience.
8. Open File Cache
Every time Nginx serves a static file, it calls open(), stat(), and close() on the filesystem. The open file cache stores file descriptors and metadata in memory, eliminating these syscalls for frequently accessed files.
http {
# Cache up to 10,000 file descriptors for 20 seconds of inactivity.
open_file_cache max=10000 inactive=20s;
# Revalidate cached entries every 30 seconds.
open_file_cache_valid 30s;
# Only cache files accessed at least twice.
# Prevents one-off requests from polluting the cache.
open_file_cache_min_uses 2;
# Cache file lookup errors (e.g., "not found").
# Prevents repeated stat() calls for missing files.
open_file_cache_errors on;
}
The open file cache is most impactful on servers with many static files (documentation sites, image galleries, CDN origins). On a site with 10,000 files, this can reduce filesystem syscalls by 90%+ and noticeably improve static file throughput. For pure reverse proxy setups without static file serving, this setting has no effect.
Monitor the impact by checking syscalls before and after with strace:
# Count open/stat syscalls on a worker for 10 seconds
sudo strace -c -p $(pgrep -f 'nginx: worker' | head -1) -e open,stat 2>&1 &
sleep 10 && kill %1
9. Upstream Optimization
When Nginx acts as a reverse proxy, the upstream configuration determines how efficiently it communicates with backend servers. The proxy cache can offload repeated requests entirely.
http {
# Define cache storage: 10 MB key zone, 1 GB disk cache.
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=app_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
upstream app_backend {
# Least connections: route to the server with fewest active requests.
least_conn;
server 127.0.0.1:3000 weight=3;
server 127.0.0.1:3001 weight=2;
server 127.0.0.1:3002 backup;
# Upstream keepalive pool.
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
server {
listen 443 ssl http2;
server_name example.com;
location /api/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Enable proxy caching for GET requests.
proxy_cache app_cache;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating;
proxy_cache_lock on;
# Add cache status header for debugging.
add_header X-Cache-Status $upstream_cache_status;
}
# Bypass cache for authenticated or dynamic requests.
location /api/user/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache off;
}
}
}
proxy_cache_lock is critical for thundering herd protection. When multiple clients request the same uncached resource simultaneously, only the first request goes to the backend — the rest wait for the cached response. proxy_cache_use_stale serves expired cache entries when the backend is down or slow, maintaining availability during outages.
10. Monitoring and Benchmarking
You cannot tune what you cannot measure. Enable stub_status for real-time metrics and use proper benchmarking tools to validate changes.
# Enable the stub_status module for basic metrics.
server {
listen 127.0.0.1:8080;
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
# Output looks like:
# Active connections: 291
# server accepts handled requests
# 16630948 16630948 31070465
# Reading: 6 Writing: 179 Waiting: 106
Benchmarking with wrk (recommended):
# Install wrk
sudo apt install wrk
# Basic benchmark: 4 threads, 200 connections, 30 seconds
wrk -t4 -c200 -d30s http://localhost/
# With custom headers (test authenticated endpoints)
wrk -t4 -c200 -d30s -H "Authorization: Bearer token123" http://localhost/api/data
# Apache Bench alternative (simpler but less capable)
ab -n 10000 -c 100 http://localhost/
# Watch live connection counts while benchmarking
watch -n1 'curl -s http://127.0.0.1:8080/nginx_status'
System-level monitoring during benchmarks:
# File descriptors used by Nginx workers
ls /proc/$(pgrep -f 'nginx: worker' | head -1)/fd | wc -l
# Network connections in various states
ss -s
# CPU and memory per Nginx worker
ps aux | grep 'nginx: worker'
# Disk I/O (if proxy_cache or temp files are in play)
iostat -x 1
Always benchmark from a separate machine on the same network. Running the load generator on the same server as Nginx means they compete for CPU, memory, and network, producing misleading results.
11. Common Mistakes and Anti-Patterns
These are the most frequent performance mistakes we see in production Nginx configs:
1. Setting worker_processes too high. More workers than CPU cores cause context switching overhead. Use auto or match your core count exactly.
# Wrong: more workers than cores wastes CPU on context switching
worker_processes 32; # on a 4-core server
# Right: match CPU cores
worker_processes auto;
2. Missing upstream keepalive. Without keepalive to your backend, every proxied request opens and closes a TCP connection. This is the single most common performance mistake in reverse proxy setups.
# Wrong: no keepalive, new connection per request
location / {
proxy_pass http://127.0.0.1:3000;
}
# Right: keepalive enabled with required headers
upstream backend {
server 127.0.0.1:3000;
keepalive 32;
}
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
3. gzip_comp_level 9. Level 9 uses 3-4x more CPU than level 5 for only 2-3% better compression. Under load, this destroys throughput.
4. Not setting worker_rlimit_nofile. If Nginx runs out of file descriptors under load, it returns 500 errors. Always set this to at least worker_connections * 2.
5. Using if for routing instead of map. The if directive inside location blocks creates a nested location context and can cause unexpected behavior. Use map for variable-based logic:
# Anti-pattern: if inside location
location / {
if ($request_uri ~* "^/api/") {
proxy_pass http://backend;
}
}
# Better: use separate location blocks
location /api/ {
proxy_pass http://backend;
}
location / {
root /var/www/site;
}
6. Buffering disabled for all proxy traffic. Disabling proxy_buffering means Nginx ties up an upstream connection for the entire duration of the client download. Only disable buffering for streaming or Server-Sent Events endpoints.
12. Real-World Tuning Checklist
Apply this checklist to any Nginx deployment. Each item references the relevant section above.
# ===== NGINX PERFORMANCE TUNING CHECKLIST =====
# 1. OS-Level
[ ] Set fs.file-max in /etc/sysctl.conf (200000+)
[ ] Set net.core.somaxconn to 65535
[ ] Update /etc/security/limits.conf for nginx user
[ ] Ensure ulimit -n matches worker_rlimit_nofile
# 2. Workers (Section 1)
[ ] worker_processes auto
[ ] worker_rlimit_nofile 65535
[ ] worker_connections 4096 (adjust to traffic)
[ ] multi_accept on
[ ] use epoll (Linux)
# 3. Network I/O (Section 4)
[ ] sendfile on
[ ] tcp_nopush on
[ ] tcp_nodelay on
# 4. Buffers (Section 2)
[ ] client_body_buffer_size 16k
[ ] proxy_buffers sized for your response payloads
[ ] Appropriate timeouts (12s client, 30s proxy)
# 5. Compression (Section 3)
[ ] gzip on with level 4-6
[ ] gzip_types includes all text formats
[ ] gzip_vary on
[ ] gzip_static on for pre-compressed assets
# 6. Caching (Sections 4, 8, 9)
[ ] Static assets: expires + Cache-Control headers
[ ] open_file_cache for static-heavy servers
[ ] proxy_cache for cacheable API responses
# 7. Keepalive (Section 5)
[ ] Client keepalive_timeout 65s
[ ] Client keepalive_requests 1000
[ ] Upstream keepalive pool sized (32-64)
[ ] proxy_http_version 1.1 + Connection ""
# 8. SSL/TLS (Section 6)
[ ] ssl_session_cache shared:SSL:10m
[ ] ssl_session_timeout 1d
[ ] ssl_stapling on
[ ] TLSv1.2 + TLSv1.3 only
[ ] HTTP/2 enabled
# 9. Security (Section 7)
[ ] Rate limiting on sensitive endpoints
[ ] limit_req with burst + nodelay
[ ] 429 status for rate-limited requests
# 10. Monitoring (Section 10)
[ ] stub_status enabled (localhost only)
[ ] Benchmark before AND after changes
[ ] Log format includes $request_time
Frequently Asked Questions
How many worker_connections should I set in Nginx?
Set worker_connections based on your expected concurrent connections per worker. The total maximum connections is worker_processes multiplied by worker_connections. For most servers, 1024 to 4096 per worker is a good starting point. A reverse proxy consumes two connections per request (one from the client, one to the backend), so divide by two for proxy scenarios. Check your OS file descriptor limit with ulimit -n and set worker_rlimit_nofile to at least twice your worker_connections. Monitor active connections via the stub_status module and adjust based on real traffic rather than guessing.
What is the best gzip_comp_level for Nginx?
A gzip_comp_level of 4 to 6 offers the best balance between compression ratio and CPU usage. Level 1 compresses fast but saves less bandwidth. Level 9 squeezes out a few extra percent of compression but uses significantly more CPU per request, which hurts throughput under load. Testing shows that level 5 or 6 achieves roughly 90% of the maximum compression at a fraction of the CPU cost of level 9. For high-traffic sites, stay at level 4 or 5. Always enable gzip_vary so CDNs and proxies cache compressed and uncompressed versions separately.
How do I enable SSL session caching in Nginx?
Add ssl_session_cache shared:SSL:10m to your http block to create a 10 MB shared cache that stores SSL session parameters across all worker processes. This avoids a full TLS handshake on every connection from returning clients. Set ssl_session_timeout 1d to keep sessions valid for 24 hours. Disable ssl_session_tickets unless you rotate ticket keys regularly, as stale ticket keys weaken forward secrecy. Enable ssl_stapling and ssl_stapling_verify so Nginx fetches OCSP responses itself rather than making clients query the certificate authority, which saves 100-300ms per new connection.
Should I use sendfile on or off in Nginx?
Enable sendfile in almost all cases. When sendfile is on, Nginx uses the kernel's sendfile() system call to transfer files directly from disk to the network socket without copying data through user space, which reduces CPU usage and improves throughput for static file serving. Combine it with tcp_nopush (which sends response headers and the beginning of a file in one TCP packet) and tcp_nodelay (which disables Nagle's algorithm for keepalive connections). The only time to disable sendfile is when serving files from a network filesystem like NFS where the kernel's sendfile implementation may cause issues.
How do I benchmark Nginx performance?
Use wrk or ab (Apache Bench) for HTTP benchmarking. wrk is preferred because it uses multiple threads and provides percentile latency data. A basic test: wrk -t4 -c200 -d30s http://your-server/ runs 4 threads, 200 connections for 30 seconds. Compare results before and after each configuration change, changing only one variable at a time. Monitor server-side metrics simultaneously using Nginx's stub_status module and system metrics (CPU with top, memory with free, network with iftop). Always benchmark from a separate machine to avoid the load generator competing for resources with Nginx.
Conclusion
Nginx performance tuning is not about applying a single magic configuration. It is about understanding your specific workload — static files vs. proxy, small payloads vs. large, few connections vs. thousands — and tuning the right knobs for that workload. Start with the checklist above, measure with stub_status and wrk, change one setting at a time, and always verify the impact with benchmarks.
The biggest wins for most deployments are: enabling upstream keepalive (20-40% throughput improvement), proper gzip compression (70-90% bandwidth reduction), SSL session caching (eliminates repeated TLS handshakes), and the sendfile/tcp_nopush/tcp_nodelay trio for static files. Apply those four changes and you will outperform 90% of Nginx deployments on the internet.
Learn More
- Nginx Configuration: Complete Guide — master server blocks, location directives, and core configuration
- Nginx Reverse Proxy Guide — proxy_pass, WebSocket proxying, and SSL termination
- Nginx Load Balancing Guide — upstream groups, health checks, and balancing algorithms
- Docker Compose Guide — containerize Nginx with your application stack
- HTTP Request Tester — test your tuned Nginx server responses and headers