<-- All Articles

Why Nginx Performance Matters

An untuned Nginx installation will serve pages, but it leaves performance on the table. The defaults are conservative by design -- they are meant to work on minimal hardware without consuming excessive resources. On a production server handling real traffic, those defaults translate to slower Time to First Byte (TTFB), unnecessary CPU cycles spent on uncompressed responses, dropped connections under load, and wasted memory from oversized buffers.

The difference between a default Nginx config and a properly tuned one is often measurable in hundreds of milliseconds per request. Multiply that across thousands of concurrent users and you are looking at degraded user experience, lower search rankings, and higher infrastructure costs from servers that could be doing more with less.

This checklist covers the directives that matter most. Each section includes the configuration block you need, what it does, and why you should care about it.

Worker Processes and Connections

Nginx uses a master process that spawns worker processes to handle requests. Each worker is single-threaded and handles connections asynchronously, so the number of workers should match your available CPU cores. Setting worker_processes to auto lets Nginx detect the core count at startup.

The worker_connections directive sets the maximum number of simultaneous connections each worker can handle. The default of 512 is low for most production workloads. Combined with worker_rlimit_nofile, which raises the file descriptor limit for worker processes, you give Nginx room to handle traffic spikes without hitting OS-level limits.

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 1024;
    multi_accept on;
}

With 4 CPU cores and 1024 connections per worker, your theoretical maximum is 4096 simultaneous connections. For most web applications behind a reverse proxy, this is more than sufficient. If you are serving as a load balancer or handling heavy WebSocket traffic, increase worker_connections to 4096 or higher.

Connection Handling

These directives control how Nginx transfers data at the kernel level. Each one removes a small overhead, and together they add up to a noticeable improvement in throughput.

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
}
  • sendfile on -- Uses the kernel's sendfile() syscall to transfer files directly from disk to the network socket, bypassing the user-space buffer copy. This is the single most important directive for serving static files.
  • tcp_nopush on -- Sends response headers and the beginning of a file in a single TCP packet rather than multiple small packets. Reduces the number of packets sent over the wire.
  • tcp_nodelay on -- Disables Nagle's algorithm on keepalive connections, ensuring small packets are sent immediately instead of being buffered. Critical for dynamic content where latency matters more than bandwidth efficiency.

Note that tcp_nopush and tcp_nodelay might seem contradictory, but Nginx applies them at different stages of the response. Headers and initial data get packed together via tcp_nopush, then tcp_nodelay takes over for the remaining data on keepalive connections.

Keepalive Connections

Every new TCP connection requires a three-way handshake, which adds latency. For users loading a page with multiple assets (CSS, JavaScript, images), keepalive connections allow the browser to reuse an existing connection instead of opening a new one for each request.

keepalive_timeout 30;
keepalive_requests 1000;

The keepalive_timeout value of 30 seconds strikes a balance between keeping connections available for rapid follow-up requests and freeing resources from idle connections. The default of 75 seconds is too generous for most sites. Setting keepalive_requests to 1000 allows up to 1000 requests over a single connection before Nginx closes it and forces a new handshake.

If you are proxying to upstream backends, also configure keepalive on the upstream block to avoid opening a new connection to your application server for every request.

Gzip Compression

Gzip compression reduces response sizes by 60-80% for text-based content. The trade-off is CPU time spent compressing, but at moderate compression levels the bandwidth savings far outweigh the processing cost.

gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_vary on;
gzip_proxied any;
gzip_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    application/rss+xml
    application/atom+xml
    image/svg+xml
    font/woff2;

A compression level of 5 provides a good balance between compression ratio and CPU usage. Levels 1-3 barely compress; levels 7-9 consume significantly more CPU with diminishing returns. The gzip_min_length 256 directive skips compression on responses smaller than 256 bytes, where the gzip overhead would actually make the response larger.

Do not add image formats like JPEG, PNG, or WebP to gzip_types. These are already compressed and attempting to gzip them wastes CPU while producing no meaningful size reduction. SVG is the exception because it is XML-based text.

Buffer Tuning

Nginx buffers client request data before passing it to the backend. The default buffer sizes work for simple GET requests, but POST requests with form data or file uploads may require larger buffers. If a request exceeds the buffer, Nginx writes it to a temporary file on disk, which is significantly slower.

client_body_buffer_size 16k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
client_max_body_size 8m;
  • client_body_buffer_size 16k -- Handles most POST request bodies in memory. Increase to 128k or higher if your application accepts large form submissions.
  • client_header_buffer_size 1k -- Sufficient for standard request headers. Most headers fit within 1KB.
  • large_client_header_buffers 4 8k -- Allocates 4 buffers of 8KB each for unusually large headers, such as those carrying long cookies or OAuth tokens.
  • client_max_body_size 8m -- Sets the maximum allowed size of a client request body. Requests exceeding this limit receive a 413 error. Adjust based on whether your application accepts file uploads.

Static File Caching

Properly configured cache headers eliminate redundant requests for static assets. When a browser has a cached copy of a CSS file or image and the cache headers say it is still valid, the browser never contacts your server at all. Zero requests are always faster than fast requests.

location ~* \.(css|js|jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|eot)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
    access_log off;
}

The expires 30d directive sets both the Expires header and the Cache-Control: max-age value to 30 days. The immutable flag tells browsers that the file will not change during its cache lifetime, preventing unnecessary revalidation requests. Use versioned filenames (e.g., app.v3.css) or query strings to bust the cache when you deploy updates.

Turning off access logging for static files is covered in the next section, but it is included here because it belongs in the same location block.

Logging Considerations

Access logging for every static asset request generates enormous log files and wastes disk I/O. A busy site serving 50 assets per page load will write 50 log lines per visit, most of which contain no useful information. Disable logging for static files in the location block shown above.

For your main access log, use buffered logging to batch writes to disk instead of flushing after every request:

access_log /var/log/nginx/access.log main buffer=16k flush=10s;

This buffers up to 16KB of log data and flushes to disk every 10 seconds or when the buffer fills, whichever comes first. The reduction in disk I/O is significant on high-traffic servers. If you are running Nginx on a VPS with limited IOPS, buffered logging can measurably improve response times during traffic spikes.

For error logging, keep the default behavior. Errors are infrequent and you want them written immediately so you can diagnose issues in real time.

Testing Your Changes

Never reload Nginx without validating your configuration first. A syntax error in any included file will prevent Nginx from starting, taking your site offline.

# Validate the configuration
nginx -t

# If the test passes, reload without downtime
nginx -s reload

After reloading, verify your changes are working. Use curl -I to inspect response headers and confirm gzip, cache headers, and keepalive are active:

curl -I -H "Accept-Encoding: gzip" https://your-domain.com/

For load testing, wrk is a lightweight benchmarking tool that generates significant traffic from a single machine:

# 4 threads, 200 connections, 30-second test
wrk -t4 -c200 -d30s https://your-domain.com/

Compare the requests-per-second and latency numbers before and after your tuning changes. Run the benchmark from a separate machine on the same network to avoid skewing results with local resource contention. The ab tool from Apache is another option, though wrk tends to produce more consistent results at higher concurrency levels.

Keep a copy of your original configuration before making changes. If performance degrades or something breaks, you can revert quickly. Version control your Nginx configs just like application code.

Need help optimizing your web server infrastructure? Learn about our DevOps automation services or schedule a consultation.

Need help with server optimization?

Our engineers tune production Nginx configurations handling millions of requests. Let us optimize your infrastructure.

Schedule a Consultation