How do you tune NGINX performance with workers, connections, and buffers?
Quick Answer
Set worker_processes to match your CPU cores, size worker_connections for your expected traffic, tune proxy and client buffers to avoid disk I/O, enable sendfile and tcp_nopush for static files, and configure keepalive to reduce connection overhead. The goal is maximum throughput with minimum latency.
Detailed Answer
Think of NGINX workers like lanes on a highway. If your server has eight CPU cores but NGINX only runs two workers, you have an eight-lane highway with six lanes closed -- traffic backs up for no reason. But running more workers than cores creates overhead from constant context switching, like cars merging between lanes nonstop. Performance tuning is about matching your NGINX config to your hardware and traffic so every resource is used without fighting for attention.
The most important setting is worker_processes, which controls how many NGINX worker processes run at once. Setting it to auto tells NGINX to count your CPU cores and spawn one worker per core, which is the best starting point. Each worker runs an event loop using epoll on Linux or kqueue on macOS/BSD, handling thousands of connections at the same time without needing a thread for each one. The worker_connections directive sets the maximum connections each worker can handle simultaneously. Your total capacity is worker_processes times worker_connections. For reverse proxy setups, remember that each client connection also needs a connection to the backend, which effectively cuts your capacity in half. Setting worker_rlimit_nofile makes sure the operating system allows enough file descriptors for all these connections.
Buffer settings have a huge effect on performance. When NGINX proxies a request, it stores the backend response in memory before sending it to the client. The proxy_buffer_size directive sets the buffer for the first part of the response, usually the headers. The proxy_buffers directive sets how many buffers to use and how big each one is for the response body. If the response is bigger than all the buffers combined, NGINX spills the overflow to a temporary file on disk, which is dramatically slower. The proxy_busy_buffers_size setting controls how much buffered data can be sent to the client while the rest is still being read from the backend. For serving static files, enabling sendfile lets the kernel handle file reads directly, skipping the usual copy through user space. Pairing it with tcp_nopush and tcp_nodelay reduces the number of TCP packets and cuts latency.
In production, tuning is an ongoing process driven by monitoring. Start with worker_processes auto and worker_connections 4096. Use stub_status or a Prometheus exporter to watch active connections, accepted connections, and request rates. If connections are queuing up, increase worker_connections and make sure worker_rlimit_nofile matches. For reverse proxy workloads, set proxy_buffers to handle your typical response size -- 8 buffers of 16k is a good default for API responses. Turn on gzip for text-based responses to save bandwidth, but watch your CPU usage since compression takes CPU cycles. Set client_body_buffer_size large enough for typical POST bodies so they stay in memory. Keepalive connections to both clients (keepalive_timeout) and backends (upstream keepalive) save the overhead of new TCP handshakes and TLS negotiations for repeat connections.
A big gotcha is setting worker_connections high without also raising the system's ulimit for file descriptors. NGINX will hit the OS limit and start refusing connections with "too many open files" errors in the log. Another common mistake is over-buffering -- setting huge proxy buffers that eat up RAM when multiplied across thousands of connections. For example, 8 buffers of 128k per connection across 10,000 connections uses nearly 10GB of RAM just for proxy buffers. Always do the math: buffer count times buffer size times expected connections equals total memory needed. Also, enabling gzip on already-compressed content like images or pre-compressed assets wastes CPU for no benefit, so use gzip_types to target only text-based content types.