LFCE 49 ๐ง Nginx โ Performance Tuning Basics
Nginx’s default configuration is conservative. It is designed to work on any hardware, from a single-core virtual machine to a large multi-core server, without requiring the administrator to know the CPU count or the memory available. That conservatism is a safety margin, not an optimization. On a production server, the default worker count, connection limit, and buffer sizes leave performance on the table. The previous chapter covered TLS configuration; this chapter covers the tuning that determines how many clients the server can handle, how efficiently it serves static files, and how much bandwidth it consumes.
Tuning is not a single change. It is a set of adjustments at three layers: the worker processes that accept connections, the event loop that manages them, and the HTTP-level directives that govern file transmission, compression, and connection reuse. The changes must be validated with nginx -t and applied with nginx -s reload, and they must be measured against the server’s actual workload rather than copied blindly from a guide .
Key point: The maximum number of simultaneous connections is approximately worker_processes ร worker_connections. worker_processes auto; sets the process count to the number of CPU cores. worker_connections sets the per-process connection limit, and worker_rlimit_nofile must be raised to match, or the operating system will refuse to open the sockets the configuration allows. The system’s ulimit -n must also be raised for the worker processes to inherit the higher limit .
Why performance tuning requires layered adjustments
The worker-process problem.
Nginx uses a master process and one or more worker processes. The workers accept connections, process requests, and send responses. A single worker process can handle thousands of connections because it uses an event-driven, non-blocking model, but it can only use one CPU core at a time. On a server with four cores, a single worker leaves three cores idle. worker_processes auto; sets the worker count to the number of detected cores, allowing the server to use all available CPU capacity . Setting it higher than the core count does not increase throughput; it increases context switching.
The file-descriptor problem.
Each connection requires a file descriptor. The default limit on many Linux systems is 1024, which caps the server at roughly 1024 simultaneous connections regardless of what worker_connections says. The worker_rlimit_nofile directive raises the limit for the worker processes, but it must be matched by the system-wide limit in /etc/security/limits.conf. If the system limit is lower than the nginx limit, the worker cannot open the file descriptors the configuration permits .
The transfer-efficiency problem.
Serving static files involves copying data from the filesystem to the kernel’s socket buffer and then to the network. The sendfile directive allows the kernel to transfer the file directly from the filesystem to the socket, bypassing the user-space copy. tcp_nopush and tcp_nodelay control how the kernel buffers and transmits the data. These directives do not change the number of connections, but they change how efficiently each connection is served, which matters for static content and large downloads .
a. Worker processes and connection limits
The worker_processes and worker_connections directives determine the theoretical maximum concurrency. The worker_rlimit_nofile directive determines whether the operating system permits it.
# /etc/nginx/nginx.conf
worker_processes auto; # One worker per CPU core
worker_rlimit_nofile 65535; # Raise the file descriptor limit
events {
worker_connections 4096; # Max connections per worker
use epoll; # Linux event model
multi_accept on; # Accept multiple connections at once
}
The use epoll; directive selects the Linux event notification mechanism. It is the default on Linux, but setting it explicitly documents the intent. The multi_accept on; directive allows a worker to accept all new connections in a single event loop iteration rather than one at a time, which improves connection acceptance under load .
The maximum concurrency calculation is approximate. With worker_processes auto on a four-core server and worker_connections 4096, the theoretical maximum is 16,384 connections. In practice, each connection consumes memory and file descriptors, and the actual limit is constrained by the server’s RAM and the system’s network stack. The worker_connections value should be raised gradually and measured, not set to an arbitrary high number .
# Verify the system file descriptor limit
ulimit -n
# The nginx worker processes inherit this limit
# If it is lower than worker_rlimit_nofile, raise it in /etc/security/limits.conf
# www-data soft nofile 65535
# www-data hard nofile 65535
b. HTTP-level transfer and connection directives
The HTTP block contains the directives that govern file transmission, connection reuse, and buffer sizes. These settings affect the efficiency of each connection rather than the number of connections.
http {
sendfile on; # Kernel-level file transfer
tcp_nopush on; # Send headers in one packet
tcp_nodelay on; # Don't buffer small packets
keepalive_timeout 65; # Idle connection timeout
keepalive_requests 1000; # Max requests per connection
client_body_timeout 12; # Slow client protection
client_header_timeout 12;
send_timeout 10;
server_tokens off; # Hide version string
types_hash_max_size 2048;
}
The sendfile on; directive is the most impactful single setting for static file serving. It reduces CPU usage and memory bandwidth by eliminating the user-space copy. The tcp_nopush on; directive, when combined with sendfile, allows nginx to send the response headers in a single packet before the file data, which reduces the number of packets on the wire .
The keepalive_timeout directive controls how long an idle connection remains open. A higher value reduces the number of TCP handshakes for clients that make multiple requests, but it holds server resources for idle connections. The default is 75 seconds; a value between 30 and 65 seconds is a common balance. The keepalive_requests directive sets the maximum number of requests a single connection can serve before it is closed. Raising it to 1000 or higher allows a connection to serve more requests before the client must reconnect .
The timeout directives protect against slow clients. A client that opens a connection and sends data slowly can hold a worker process for an extended period. The client_body_timeout and client_header_timeout directives close the connection if the client does not send the complete request within the specified time .
c. Gzip compression and static asset caching
Gzip compression reduces the size of text-based responses. Static asset caching tells the browser to store files locally so that subsequent requests do not reach the server at all.
http {
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5; # Balance CPU vs compression
gzip_min_length 1024; # Don't compress tiny files
gzip_types
text/plain
text/css
application/json
application/javascript
text/xml
application/xml
image/svg+xml;
}
server {
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
}
The gzip_comp_level directive ranges from 1 to 9. Higher levels compress more but consume more CPU. Level 5 or 6 is the common balance for production servers . The gzip_min_length directive prevents compression of very small files, where the compression overhead exceeds the savings. The gzip_types directive lists the MIME types that are compressed; images and videos are already compressed and should not be included .
The expires directive in the static asset location sets the Expires and Cache-Control headers. A value of 30d or 365d tells the browser that the file is valid for that duration. The immutable directive in the Cache-Control header tells the browser that the file will never change, which allows it to skip revalidation entirely. The access_log off; directive disables logging for static assets, which reduces disk I/O on high-traffic servers .
Complete Example Session
# ============================================
# PART 1: INSPECT THE CURRENT CONFIGURATION
# ============================================
grep -E "worker_processes|worker_connections|sendfile|gzip" /etc/nginx/nginx.conf
# The default values are conservative
# ============================================
# PART 2: CHECK THE SYSTEM FILE DESCRIPTOR LIMIT
# ============================================
ulimit -n
# Output: 1024 (the default, which is too low for high concurrency)
# ============================================
# PART 3: RAISE THE SYSTEM LIMIT
# ============================================
sudo tee -a /etc/security/limits.conf > /dev/null <<'EOF'
www-data soft nofile 65535
www-data hard nofile 65535
EOF
# ============================================
# PART 4: EDIT THE NGINX CONFIGURATION
# ============================================
sudo tee /etc/nginx/nginx.conf > /dev/null <<'EOF'
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000;
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
server_tokens off;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
EOF
# ============================================
# PART 5: ADD STATIC ASSET CACHING
# ============================================
# In the site's server block or a location block
# location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
# expires 30d;
# add_header Cache-Control "public, immutable";
# access_log off;
# }
# ============================================
# PART 6: VALIDATE THE CONFIGURATION
# ============================================
sudo nginx -t
# Output: syntax is ok, test is successful
# ============================================
# PART 7: RELOAD NGINX
# ============================================
sudo systemctl reload nginx
# ============================================
# PART 8: VERIFY THE WORKER COUNT
# ============================================
ps aux | grep "nginx: worker" | grep -v grep | wc -l
# Output: the number of CPU cores
# ============================================
# PART 9: VERIFY GZIP IS ACTIVE
# ============================================
curl -H "Accept-Encoding: gzip" -I http://localhost
# Output includes: Content-Encoding: gzip
# ============================================
# PART 10: MEASURE BEFORE AND AFTER
# ============================================
# Use a load testing tool such as ab, wrk, or hey
# ab -n 10000 -c 100 http://localhost/
# Compare requests per second and latency before and after tuning
The ten parts demonstrate the tuning workflow: inspect, raise the system limit, edit the configuration, add caching, validate, reload, verify the worker count, verify gzip, and measure. The critical step is Part 3: raising the system limit is necessary before nginx can use the higher worker_rlimit_nofile value. Without it, the worker processes inherit the old limit and the connection ceiling remains unchanged.
Quick Reference
Worker and Event Directives
| Directive | Recommended Value | Purpose |
|---|---|---|
worker_processes | auto | One worker per CPU core |
worker_rlimit_nofile | 65535 | File descriptor limit per worker |
worker_connections | 4096 or higher | Connections per worker |
use | epoll | Linux event model |
multi_accept | on | Accept multiple connections at once |
HTTP Transfer Directives
| Directive | Recommended Value | Purpose |
|---|---|---|
sendfile | on | Kernel-level file transfer |
tcp_nopush | on | Send headers in one packet |
tcp_nodelay | on | Disable Nagle for small packets |
keepalive_timeout | 30โ65 | Idle connection timeout |
keepalive_requests | 1000 | Max requests per connection |
Gzip and Caching
| Directive | Recommended Value | Purpose |
|---|---|---|
gzip | on | Enable compression |
gzip_comp_level | 5 or 6 | Balance CPU vs compression |
gzip_min_length | 1024 | Skip tiny files |
gzip_types | Text MIME types | Compress only text |
expires | 30d or 365d | Static asset cache duration |
Best Practices
โ Do This:
# Set worker_processes to auto
worker_processes auto; # โ
# Raise the file descriptor limit
worker_rlimit_nofile 65535; # โ
# Enable sendfile for static files
sendfile on; # โ
# Enable gzip for text resources
gzip on; gzip_comp_level 5; # โ
# Cache static assets with long expiry
expires 30d; add_header Cache-Control "public, immutable"; # โ
# Validate before reloading
nginx -t # โ
# Measure the impact with a load testing tool
ab -n 10000 -c 100 http://localhost/ # โ
โ Don’t Do This:
# Set worker_processes higher than the core count
worker_processes 16; # On a 4-core server โ context switching # โ
# Set worker_connections without raising worker_rlimit_nofile
worker_connections 10000; # System limit caps it at 1024 # โ
# Compress images with gzip
gzip_types image/jpeg image/png; # Already compressed # โ
# Set gzip_comp_level to 9 for all traffic
gzip_comp_level 9; # High CPU cost for marginal gain # โ
# Disable keepalive entirely
keepalive_timeout 0; # Every request requires a new handshake # โ
# Apply tuning without measuring
# Changes may not improve the specific workload # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Connection ceiling lower than expected | System ulimit -n is too low | Raise limit in /etc/security/limits.conf |
| Workers exceed CPU cores | worker_processes set manually too high | Use auto |
| High CPU with gzip | gzip_comp_level too high | Reduce to 5 or 6 |
| Static assets not cached | No expires header | Add expires and Cache-Control |
| Slow client holds connections | No timeout directives | Set client_body_timeout and client_header_timeout |
| Configuration error after tuning | Missing semicolon or invalid directive | nginx -t reports the line |
Real-World Examples
1. Worker auto-detection
worker_processes auto;
2. Connection limit
worker_connections 4096;
3. File descriptor limit
worker_rlimit_nofile 65535;
4. Event model
use epoll; multi_accept on;
5. Efficient file transfer
sendfile on; tcp_nopush on; tcp_nodelay on;
6. Keepalive settings
keepalive_timeout 65; keepalive_requests 1000;
7. Gzip compression
gzip on; gzip_comp_level 5; gzip_min_length 1024;
8. Static asset caching
location ~* \.(css|js|jpg|png)$ { expires 30d; add_header Cache-Control "public, immutable"; }
9. Validate configuration
nginx -t
10. Measure performance
ab -n 10000 -c 100 http://localhost/
Visual
The Concurrency Formula
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MAX CONNECTIONS โ worker_processes ร worker_connections โ
โ โ
โ 4 CPU cores โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ worker_processes = 4 (auto) โ โ
โ โ worker_connections = 4096 โ โ
โ โ โ โ
โ โ Theoretical max = 4 ร 4096 = 16,384 connections โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ BUT: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ worker_rlimit_nofile must be โฅ worker_connections โ โ
โ โ System ulimit -n must be โฅ worker_rlimit_nofile โ โ
โ โ Otherwise the system limit caps the actual connections โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Three Layers of Tuning
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TUNING IS LAYERED โ
โ โ
โ Layer 1: Process and connection โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ worker_processes, worker_connections, โ โ
โ โ worker_rlimit_nofile, use epoll โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ Layer 2: Transfer and connection reuse โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ sendfile, tcp_nopush, tcp_nodelay, โ โ
โ โ keepalive_timeout, keepalive_requests โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ Layer 3: Bandwidth and caching โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ gzip, expires, Cache-Control, access_log off โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Each layer addresses a different bottleneck. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Static Asset Caching Headers
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CACHE-CONTROL AND EXPIRES REDUCE SERVER LOAD โ
โ โ
โ Without caching: โ
โ Browser โโโถ Server: "Give me style.css" โ
โ Server โโโถ Browser: "Here it is" โ
โ (every request reaches the server) โ
โ โ
โ With caching: โ
โ Browser: "I have style.css, expires in 30 days" โ
โ (subsequent requests use the local copy, no server contact) โ
โ โ
โ Headers: โ
โ expires 30d; โ
โ add_header Cache-Control "public, immutable"; โ
โ โ
โ The immutable directive tells the browser it can skip โ
โ revalidation entirely for the cached file. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Worker processes | worker_processes auto; |
| Per-worker connections | worker_connections 4096; |
| File descriptor limit | worker_rlimit_nofile 65535; |
| System limit | /etc/security/limits.conf |
| Event model | use epoll; multi_accept on; |
| File transfer | sendfile on; tcp_nopush on; tcp_nodelay on; |
| Keepalive | keepalive_timeout 65; keepalive_requests 1000; |
| Gzip | gzip on; gzip_comp_level 5; gzip_min_length 1024; |
| Static caching | expires 30d; add_header Cache-Control "public, immutable"; |
| Validation | nginx -t |
| Measurement | ab, wrk, or hey |
Key takeaways:
- The concurrency ceiling is
worker_processes ร worker_connections.worker_processes automatches the core count, andworker_connectionssets the per-worker limit. The system’s file descriptor limit must be raised to match, or the configuration is capped by the operating system . sendfile,tcp_nopush, andtcp_nodelayimprove transfer efficiency. These directives reduce CPU usage and packet count for static files. They do not change the connection count, but they change how efficiently each connection is served .- Keepalive reduces handshake overhead.
keepalive_timeoutandkeepalive_requestscontrol how long a connection stays open and how many requests it can serve. A higherkeepalive_requestsvalue is appropriate for high-traffic servers . - Gzip reduces bandwidth for text resources. The compression level should balance CPU cost against savings. Level 5 or 6 is a common production value, and
gzip_min_lengthprevents compression of files where the overhead exceeds the benefit . - Static asset caching eliminates repeat requests. The
expiresandCache-Controlheaders tell the browser to store the file locally. Theimmutabledirective allows the browser to skip revalidation entirely . - Measure before and after. The values in this chapter are starting points, not universal recommendations. The actual impact depends on the server’s hardware, the workload, and the network. A load testing tool and the access logs provide the data that determines whether the tuning achieved its goal .
Remember: Nginx performance tuning is a matter of matching the configuration to the hardware and the workload. The defaults are safe but conservative. worker_processes auto uses the available cores. worker_connections and worker_rlimit_nofile raise the connection ceiling, but only if the system’s file descriptor limit is raised to match. sendfile and the TCP directives make each connection more efficient. Gzip and static caching reduce bandwidth and server load. Validate with nginx -t, reload with systemctl reload nginx, and measure with a load testing tool. The configuration that improves one server may not improve another, because the bottleneck is different.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!