Kernel and OS Tuning for NGINX
Learn NGINX In Action - Part 096
Kernel dan OS tuning untuk NGINX production: file descriptors, worker limits, socket queues, TCP behavior, disk/filesystem, cgroup, container, NUMA, dan safe rollout methodology.
Part 096 — Kernel and OS Tuning for NGINX
Goal part ini: memahami OS/kernel tuning yang benar-benar memengaruhi NGINX, tanpa cargo-cult sysctl.
NGINX berjalan di atas kernel. Worker NGINX memang event-driven, tetapi ia tetap bergantung pada:
- process limits,
- file descriptors,
- socket queues,
- TCP state machine,
- listen backlog,
- ephemeral ports,
- filesystem dan page cache,
- disk scheduler/storage latency,
- cgroup/container limits,
- CPU scheduling,
- NUMA topology,
- network interface dan NIC queues.
Kesalahan umum:
“Tambahkan sysctl tuning dari blog internet, restart, selesai.”
Itu berbahaya.
OS tuning bukan ritual. OS tuning adalah proses menyelaraskan tiga hal:
- workload NGINX,
- batas kernel/host/container,
- kapasitas upstream dan dependency.
1. Prinsip utama OS tuning
Prinsip 1 — Tune boundary yang terbukti menjadi bottleneck
Jangan menaikkan semuanya.
Contoh:
- Jika FD exhaustion terjadi, tune file descriptor limit.
- Jika listen queue overflow terjadi, tune backlog dan accept capacity.
- Jika outbound upstream connection gagal karena ephemeral port exhaustion, tune port range/reuse strategy/keepalive/NAT path.
- Jika disk cache lambat, tune storage layout, filesystem, dan cache/temp paths.
- Jika CPU imbalance, tune worker count/affinity only if evidence supports it.
Prinsip 2 — NGINX limit dan OS limit harus cocok
NGINX config tidak bisa melampaui OS limit.
Raising only one layer creates misleading config.
Prinsip 3 — Bigger queues are not always safer
Queue besar dapat mengurangi connection drops, tetapi juga bisa menambah tail latency.
Antrian adalah buffer. Buffer menyembunyikan overload sementara, bukan menambah kapasitas service.
Prinsip 4 — Container limit is real limit
Jika NGINX berjalan di container, host terlihat besar tetapi cgroup bisa kecil.
Yang harus dicek:
cat /sys/fs/cgroup/cpu.max 2>/dev/null || true
cat /sys/fs/cgroup/memory.max 2>/dev/null || true
cat /sys/fs/cgroup/pids.max 2>/dev/null || true
cat /proc/self/limits
2. File descriptor model
NGINX memakai file descriptor untuk:
- client sockets,
- upstream sockets,
- listening sockets,
- open static files,
- log files,
- cache files,
- temp files,
- event mechanism resources,
- DNS resolver sockets,
- module-specific files/sockets.
2.1 NGINX-level directives
worker_processes auto;
worker_rlimit_nofile 200000;
events {
worker_connections 32768;
}
Important:
worker_processes × worker_connections != guaranteed client capacity
For reverse proxy traffic, a single user request can require both a client socket and upstream socket.
2.2 systemd limit
For package-based deployments, systemd often owns the actual process limit.
Check:
systemctl show nginx --property=LimitNOFILE
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
Override:
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=200000
Apply:
sudo systemctl daemon-reload
sudo systemctl restart nginx
2.3 kernel global file limit
Check:
sysctl fs.file-max
cat /proc/sys/fs/file-nr
Interpretation of /proc/sys/fs/file-nr:
allocated unused maximum
If global file table is exhausted, not only NGINX suffers.
2.4 Sizing FD limit
Rough model:
fd_needed = client_connections
+ upstream_connections
+ upstream_idle_keepalive
+ open_files
+ logs
+ cache/temp activity
+ 20-30% safety margin
Example:
client connections: 40,000
active upstream sockets: 25,000
idle upstream keepalive: 5,000
files/log/cache/temp: 5,000
margin 25%: 18,750
---------------------------------
recommended: 93,750+
Set process limit higher than recommendation, but do not forget memory and backend capacity.
3. Worker process and CPU scheduling
3.1 worker_processes auto
For most production systems:
worker_processes auto;
This is usually the right starting point.
But “one worker per CPU” is not always optimal when:
- container CPU quota is smaller than visible host CPUs,
- heavy WAF/module memory makes too many workers expensive,
- CPU pinning/NUMA topology matters,
- workload is mostly I/O-bound and constrained elsewhere,
- Kubernetes pod CPU limits throttle workers.
Check visible CPU and cgroup quota:
nproc
lscpu
cat /sys/fs/cgroup/cpu.max 2>/dev/null || true
cat /sys/fs/cgroup/cpu.stat 2>/dev/null || true
In cgroup v2, cpu.max format:
quota period
Example:
200000 100000
means roughly 2 CPUs.
If NGINX sees 32 host CPUs but container has 2 CPU quota, worker_processes auto behavior must be validated in your runtime/version/distribution.
3.2 Worker CPU affinity
Manual affinity can help in specialized high-throughput environments, but it is not first-line tuning.
Reasons to avoid early affinity:
- Kubernetes scheduler may move containers.
- CPU quota can throttle anyway.
- NUMA topology may be unknown.
- IRQ affinity may dominate network packet distribution.
- Wrong pinning can make imbalance worse.
Investigate first:
ps -o pid,psr,pcpu,cmd -C nginx
mpstat -P ALL 1
cat /proc/interrupts | head -40
Use affinity only when you can prove worker imbalance and you control host topology.
4. Listen backlog and accept queues
When clients connect, kernel queues connection attempts before NGINX accepts them.
Important layers:
4.1 NGINX listen backlog
server {
listen 443 ssl backlog=65535;
}
The effective backlog is also bounded by kernel settings.
4.2 Kernel somaxconn
Check:
sysctl net.core.somaxconn
Set via sysctl file:
# /etc/sysctl.d/99-nginx.conf
net.core.somaxconn = 65535
Apply:
sudo sysctl --system
4.3 SYN backlog
Check:
sysctl net.ipv4.tcp_max_syn_backlog
Potential tuning:
net.ipv4.tcp_max_syn_backlog = 65535
But large backlog alone does not solve application overload. It delays rejection.
4.4 Detect listen queue overflow
nstat -az | egrep 'ListenOverflows|ListenDrops|Syncookies|TCPReqQFull'
ss -ltn sport = :443
If listen overflows rise during traffic spikes:
- NGINX workers may not accept fast enough,
- CPU may be saturated,
- worker count may be too low,
- backlog too small,
- SYN flood/attack may be happening,
- downstream app does not matter yet because connection cannot enter NGINX.
5. Ephemeral ports and outbound upstream connections
Reverse proxy creates outbound connections to upstreams unless reusing keepalive.
Each outbound TCP connection uses:
source_ip:source_port -> destination_ip:destination_port
If NGINX opens many short-lived connections to the same upstream tuple, ephemeral ports can become a boundary.
Check range:
sysctl net.ipv4.ip_local_port_range
Typical output:
32768 60999
That gives around 28k local ports per source IP per destination tuple.
5.1 Symptoms
- connect failures under high churn,
- many
TIME-WAITsockets, $upstream_connect_timeincreases,connect() failederrors,- NAT gateway exhaustion if traffic leaves host/VPC path.
Diagnostics:
ss -tan state time-wait | wc -l
ss -tan state syn-sent | wc -l
ss -tan dst <upstream_ip>:<port> | awk '{print $1}' | sort | uniq -c
sar -n TCP,ETCP 1
5.2 Better first fix: upstream keepalive
Before tuning port range, reduce churn:
upstream api_backend {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
keepalive 256;
}
location /api/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://api_backend;
}
5.3 Port range tuning
net.ipv4.ip_local_port_range = 10240 65535
But do not treat this as universal fix.
Also consider:
- source IP sharding,
- more upstream endpoints,
- keepalive,
- avoiding NAT bottlenecks,
- backend accept queue,
- connection pooling at app layer,
- HTTP/2 upstream if applicable and supported by architecture.
6. TCP TIME_WAIT, reuse, and dangerous myths
Many TIME-WAIT sockets are not automatically bad. TIME-WAIT is part of TCP correctness.
Do not blindly tune TCP state behavior because a dashboard shows many TIME_WAIT sockets.
Ask:
- Are connections failing?
- Is ephemeral port range exhausted?
- Is NAT device exhausted?
- Is upstream keepalive disabled?
- Are clients/upstreams closing connections aggressively?
- Is traffic pattern short-lived by design?
Safer levers first:
- enable upstream keepalive where appropriate,
- reduce unnecessary connection churn,
- increase upstream endpoint diversity,
- increase ephemeral port range if proven needed,
- avoid NAT bottleneck for high-volume east-west traffic.
7. Network buffers and socket memory
You will see tuning advice like:
net.core.rmem_max = ...
net.core.wmem_max = ...
net.ipv4.tcp_rmem = ...
net.ipv4.tcp_wmem = ...
These can matter for high bandwidth-delay-product paths or large transfers, but they are not first-line NGINX tuning for typical API workloads.
Use them only when evidence shows socket buffer limitation.
Useful probes:
ss -tin sport = :443 | head -40
sar -n DEV,TCP,ETCP 1
ip -s link
etstat -s | egrep -i 'buffer|retrans|listen|reset|timeout'
If retransmits are high, increasing buffers may hide but not fix packet loss.
8. sendfile, page cache, and static files
NGINX static file performance is heavily shaped by Linux page cache and file I/O path.
Relevant NGINX directives:
sendfile on;
tcp_nopush on;
tcp_nodelay on;
open_file_cache max=20000 inactive=60s;
open_file_cache_valid 120s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
OS implications:
- Frequently accessed static assets should be served from page cache.
- Cold cache tests are different from warm cache tests.
- Disk latency matters during cold start or cache miss.
- Container overlay filesystems may behave differently from host-mounted volumes.
sendfilecan reduce user-space copy overhead, but always test with your TLS/filesystem/container combination.
Inspect page cache indirectly:
free -h
vmtouch /srv/www 2>/dev/null || true
sar -B 1
Do not clear page cache in production to “test fairly”.
9. Disk and filesystem tuning
NGINX disk I/O comes from:
- access logs,
- error logs,
- static files,
- proxy temp files,
- client body temp files,
- FastCGI/uWSGI/SCGI temp files,
- proxy cache,
- cache loader/manager.
9.1 Separate high-risk paths
Recommended conceptual split:
/var/log/nginx logs
/var/cache/nginx proxy cache
/var/lib/nginx/tmp proxy/client temp files
/srv/www static assets
Better: separate filesystem/volume when workload is heavy.
Why?
- cache filling disk should not stop logging,
- upload temp pressure should not kill cache,
- log spike should not break file serving,
- static assets should not compete with temp writes.
9.2 Monitor inode, not only bytes
df -h
df -i
find /var/cache/nginx -type f | wc -l
Small-object cache can exhaust inodes before bytes.
9.3 use_temp_path=off
For proxy cache:
proxy_cache_path /var/cache/nginx/api
levels=1:2
keys_zone=api_cache:100m
max_size=20g
inactive=1h
use_temp_path=off;
use_temp_path=off keeps temporary cache write files in the same directory tree as the cache path. This can avoid cross-filesystem rename/copy cost.
9.4 Logging pressure
Logging can be a bottleneck.
Options:
- reduce log fields only after preserving RCA needs,
- conditional logging for noisy low-value endpoints,
- buffer logs carefully,
- ship logs asynchronously,
- use local disk as buffer before remote shipping,
- avoid synchronous remote log path on critical request thread.
Example conditional health check suppression:
map $uri $loggable {
default 1;
/healthz 0;
}
access_log /var/log/nginx/access.json main_json if=$loggable;
Do not suppress logs needed for incident reconstruction.
10. Memory, cgroups, and OOM safety
10.1 Container memory limit
Check:
cat /sys/fs/cgroup/memory.max 2>/dev/null || true
cat /sys/fs/cgroup/memory.current 2>/dev/null || true
cat /sys/fs/cgroup/memory.events 2>/dev/null || true
If memory.max is small, NGINX may be OOM-killed even if host memory looks fine.
10.2 Common memory consumers
| Consumer | Cause |
|---|---|
| connection state | high concurrent clients/upstreams |
| proxy buffers | response buffering |
| request body buffers | uploads |
| header buffers | large headers/cookies |
| cache metadata | keys_zone size |
| modules | WAF, njs, Lua, dynamic modules |
| TLS | session cache, cert/key handling, OpenSSL state |
| OS page cache | static/cache files |
10.3 Avoid swap dependency
For latency-sensitive edge nodes, swapping worker memory is usually catastrophic.
Check:
vmstat 1
free -h
cat /proc/swaps
If swap activity appears during traffic, reduce memory pressure or raise memory limit. Do not rely on swap as normal operating mode.
11. CPU, IRQs, NIC queues, and packet processing
At high traffic, packet processing can become CPU-bound outside NGINX.
Inspect:
mpstat -P ALL 1
cat /proc/interrupts | egrep 'eth|ens|eno|mlx|ixgbe|ena' | head -40
ethtool -l eth0 2>/dev/null || true
ethtool -S eth0 2>/dev/null | head -80 || true
Potential issues:
- one CPU handles most NIC interrupts,
- RSS not configured or not available,
- softirq saturation,
- packet drops at NIC or kernel,
- cloud instance network cap,
- conntrack/NAT overhead.
NGINX tuning cannot fix NIC queue imbalance by itself.
12. Conntrack and NAT path
If NGINX sits behind or in front of NAT/firewall/ Kubernetes iptables path, conntrack can be a hidden boundary.
Symptoms:
- intermittent connect failures,
- packet drops with low NGINX CPU,
- NAT gateway port exhaustion,
- high conntrack table usage,
- Kubernetes node-level network bottleneck.
Check Linux conntrack if applicable:
sysctl net.netfilter.nf_conntrack_max 2>/dev/null || true
cat /proc/sys/net/netfilter/nf_conntrack_count 2>/dev/null || true
cat /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null || true
Do not raise conntrack blindly. Ask whether traffic should bypass NAT or use more direct routing.
13. Safe sysctl baseline template
This is not universal. Treat it as a review starting point, not a paste-and-pray config.
# /etc/sysctl.d/99-nginx-edge.conf
# Larger accept queue ceiling. Effective only when app/listen backlog also aligns.
net.core.somaxconn = 65535
# Larger SYN backlog for high connection rate environments.
net.ipv4.tcp_max_syn_backlog = 65535
# Wider outbound ephemeral port range for high-churn reverse proxy workloads.
net.ipv4.ip_local_port_range = 10240 65535
# Global file table. Must align with process limits and host capacity.
fs.file-max = 1000000
Apply:
sudo sysctl --system
Validate:
sysctl net.core.somaxconn \
net.ipv4.tcp_max_syn_backlog \
net.ipv4.ip_local_port_range \
fs.file-max
What this does not solve:
- upstream slowness,
- backend connection pool exhaustion,
- disk I/O saturation,
- TLS CPU saturation,
- bad cache key,
- retry storms,
- WAF CPU cost,
- Kubernetes service routing bottlenecks,
- cloud load balancer limits.
14. systemd production baseline
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=200000
TasksMax=infinity
Then:
sudo systemctl daemon-reload
sudo systemctl restart nginx
systemctl show nginx --property=LimitNOFILE --property=TasksMax
cat /proc/$(pgrep -o nginx)/limits
Caution:
TasksMaxusually not a bottleneck for NGINX itself because worker process count is modest.- FD limit matters much more.
- Restart may be required for process limit changes; reload is not always enough because master process inherited old limits.
15. NGINX config baseline aligned with OS
user nginx;
worker_processes auto;
worker_rlimit_nofile 200000;
events {
worker_connections 32768;
multi_accept off;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30s;
keepalive_requests 1000;
client_header_timeout 10s;
client_body_timeout 30s;
send_timeout 30s;
access_log /var/log/nginx/access.json main_json;
error_log /var/log/nginx/error.log warn;
}
Notes:
multi_accept oncan improve accepting bursts but may hurt fairness; do not enable blindly.accept_mutexdefault behavior changed historically; verify current version and workload before tuning.keepalive_timeouttoo high can retain many idle connections.keepalive_requestsbounds reuse lifecycle and resource retention.
16. Kubernetes/container-specific OS tuning
In Kubernetes, an NGINX pod may not be allowed to tune host sysctl freely.
Important boundaries:
- pod CPU request/limit,
- pod memory request/limit,
- file descriptor limit from container runtime,
- node-level sysctl,
- Service/load balancer path,
- conntrack table,
- CNI plugin behavior,
- readiness/liveness probes,
- ingress controller reload behavior.
Check inside pod:
cat /proc/self/limits
cat /sys/fs/cgroup/cpu.max 2>/dev/null || true
cat /sys/fs/cgroup/memory.max 2>/dev/null || true
ss -s
Check node if allowed:
sysctl net.core.somaxconn
sysctl net.ipv4.tcp_max_syn_backlog
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
Anti-pattern:
Set NGINX worker_connections to 100k inside a pod with 256Mi memory and tiny CPU limit.
Container limits must be part of capacity planning.
17. Cloud load balancer and proxy chain limits
NGINX is often not the first or last hop.
Path example:
Each hop may have:
- idle timeout,
- max header size,
- TLS policy,
- HTTP/2 behavior,
- proxy protocol support,
- connection reuse behavior,
- request body limit,
- health check behavior,
- rate limit,
- log sampling.
OS tuning NGINX cannot override cloud load balancer connection limits or NAT gateway limits.
For production review, document every hop:
| Hop | Idle timeout | Header limit | Body limit | Protocol | Real IP mechanism | Health check |
|---|---:|---:|---:|---|---|---|
| CDN | | | | | | |
| Cloud LB | | | | | | |
| NGINX | | | | | | |
| App LB | | | | | | |
| App | | | | | | |
18. Rollout method for OS tuning
OS tuning can impact the whole host.
Use canary method:
- Pick one node/instance.
- Capture baseline.
- Apply one sysctl/process limit change.
- Restart/reload only as required.
- Run synthetic and real canary traffic.
- Compare:
- p95/p99,
- error rate,
- CPU,
- memory,
- disk,
- retransmits,
- listen drops,
- FD usage,
- connection states,
- upstream health.
- Roll out gradually.
- Keep rollback file.
Rollback example:
sudo mv /etc/sysctl.d/99-nginx-edge.conf /etc/sysctl.d/99-nginx-edge.conf.disabled
sudo sysctl --system
sudo systemctl restart nginx
For systemd limit rollback:
sudo rm /etc/systemd/system/nginx.service.d/override.conf
sudo systemctl daemon-reload
sudo systemctl restart nginx
19. OS tuning checklist
File descriptors
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
cat /proc/sys/fs/file-nr
lsof -p <worker_pid> | wc -l
Question:
- Is FD limit above expected peak plus margin?
- Do upstream sockets double FD demand?
- Are logs/cache/temp files included?
Accept queues
nstat -az | egrep 'ListenOverflows|ListenDrops|Syncookies'
ss -ltn sport = :80
ss -ltn sport = :443
Question:
- Are listen drops increasing during spikes?
- Is backlog aligned with
somaxconn? - Is CPU/worker accept speed the real boundary?
Ephemeral ports
sysctl net.ipv4.ip_local_port_range
ss -tan state time-wait | wc -l
ss -tan state syn-sent | wc -l
Question:
- Is upstream keepalive enabled?
- Is port exhaustion real or only TIME_WAIT visible?
- Is NAT in path?
Disk
iostat -xz 1
df -h
df -i
pidstat -d -p $(pidof nginx | tr ' ' ',') 1
Question:
- Are logs/cache/temp isolated?
- Is disk await high?
- Are inodes enough?
- Is cache on suitable volume?
Memory
free -h
vmstat 1
cat /sys/fs/cgroup/memory.events 2>/dev/null || true
Question:
- Is NGINX hitting container memory limit?
- Is swap active?
- Are buffers/cache zones too large?
- Are long-lived connections accumulating?
Network
sar -n DEV,TCP,ETCP 1
ip -s link
nstat -az | egrep 'Retrans|Reset|Timeout|Drop|Overflow'
Question:
- Is link saturated?
- Are retransmits high?
- Are NIC drops occurring?
- Is cloud instance network cap reached?
20. Things not to do
Do not paste giant sysctl recipes
Many recipes were written for different kernels, workloads, NICs, or cloud environments.
Do not disable TCP correctness features casually
TCP state handling exists for correctness. Shortening state lifetimes blindly can create rare, severe bugs.
Do not increase every buffer
Bigger buffers can increase memory use and worsen OOM risk.
Do not set unlimited everything
Unlimited FD, memory, logs, cache, and connections can turn overload into host-level failure.
Do not tune NGINX beyond upstream capacity
If backend can handle 5k concurrent requests, allowing NGINX to send 50k concurrent upstream requests is not resilience.
Do not ignore p99
Average latency hides queueing and saturation.
21. Minimal safe starting profile
For a medium production reverse proxy, a conservative starting point might be:
worker_processes auto;
worker_rlimit_nofile 100000;
events {
worker_connections 16384;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30s;
keepalive_requests 1000;
client_header_timeout 10s;
client_body_timeout 30s;
send_timeout 30s;
}
Systemd:
[Service]
LimitNOFILE=100000
Sysctl:
net.core.somaxconn = 32768
net.ipv4.tcp_max_syn_backlog = 32768
net.ipv4.ip_local_port_range = 10240 65535
fs.file-max = 500000
But this is not “best practice” universally. It is merely a starting profile that must be validated against:
- traffic,
- memory,
- CPU,
- upstream capacity,
- cloud LB behavior,
- deployment substrate,
- SLO.
22. Final mental model
NGINX OS tuning is not about making numbers large.
It is about alignment:
NGINX config limit
<= process limit
<= kernel/host/container capacity
<= upstream capacity
<= SLO and overload policy
When these limits are aligned, NGINX fails predictably.
When they are not aligned, NGINX fails mysteriously:
- 502/504 without clear cause,
- random connect failures,
- OOM kills,
- disk full incidents,
- p99 spikes,
- cache storms,
- SYN drops,
- NAT exhaustion,
- app overload caused by proxy retry.
Production tuning is less about “maximum performance” and more about making the edge boring under pressure.
That is the goal.
References
- NGINX Core Module:
worker_processes,worker_rlimit_nofile,worker_connections,accept_mutex,multi_accept, and event loop directives. - NGINX HTTP Core Module:
sendfile,tcp_nopush,tcp_nodelay,keepalive_timeout, static serving and buffering-related directives. - NGINX Proxy Module: proxy buffering, temp files, upstream timeout and connection behavior.
- NGINX Upstream Module: upstream keepalive and upstream timing variables.
- NGINX/F5 tuning guidance: NGINX and Linux settings that can affect performance, with the warning that tuning is workload-specific.
- Linux kernel documentation and runtime inspection tools:
sysctl,ss,sar,nstat,iostat,vmstat,pidstat, cgroup files.
You just completed lesson 96 in final stretch. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.