Performance Tuning & Troubleshooting

Nginx In Depth

Chapter 9 · Performance Tuning & Troubleshooting

Web Servers Fundamentals Chapter 9 covered keep-alive and gzip at a general, three-server level. This chapter is Nginx-specific: how it actually moves bytes off disk and onto the network, how it manages a backend response before handing it to a client, and the tools to find out what's actually happening when something's slow.

sendfile, tcp_nopush & tcp_nodelay

sendfile on; tcp_nopush on; tcp_nodelay on;

sendfile lets the kernel copy a static file's data directly to the network socket, without that data passing through Nginx's own userspace process memory first — a real, meaningful win specifically for serving static files. tcp_nopush tells the kernel to wait and accumulate a full network packet's worth of data before sending, working alongside sendfile to batch data efficiently. tcp_nodelay does the seemingly opposite thing — disabling Nagle's algorithm so small packets are sent immediately rather than held back to be batched. The pairing isn't actually contradictory: Nginx uses tcp_nopush to batch data efficiently through most of a response, then tcp_nodelay to flush the final packet immediately rather than waiting — both enabled together is the correct, common configuration, not a conflict to resolve.

Proxy Buffering

proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k;

By default, Nginx buffers a backend's response before sending it on to the client — letting the backend connection close quickly even if the client itself is slow to receive the data, rather than holding that backend connection open for the full duration of a slow client transfer. proxy_buffer_size sets the buffer for the response headers specifically; proxy_buffers sets the number and size of buffers used for the response body. A response larger than the configured buffer space spills over to a temporary file on disk, which can introduce real disk I/O the config didn't anticipate if buffers are sized too small for typical response sizes.

stub_status: A Real-Time Window Into Nginx

location /nginx_status { stub_status; allow 127.0.0.1; deny all; }

The stub_status module exposes a simple plain-text endpoint showing active connections right now, and running totals for accepted/handled/total requests, plus how many connections are currently reading, writing, or waiting. It's a lightweight, immediate diagnostic tool — genuinely useful as a first stop, though it's not a substitute for a real observability stack (Observability's own material) that retains history, alerts, and correlates across multiple systems.

Diagnosing Common Misconfigurations

  • Check the error log first (Web Servers Fundamentals Chapter 8) — many misconfigurations announce themselves there directly.
  • Check stub_status to see whether Nginx itself is under load, or whether the real bottleneck is a slow backend it's simply waiting on.
  • Check X-Cache-Status (Chapter 7) to rule caching in or out of a given slow response.
  • Check whether worker_connections or the OS file-descriptor limit (Chapter 2) is silently being hit, rather than assuming the problem is purely about traffic volume.
DirectivePurpose
sendfileKernel sends file data directly, bypassing userspace copy
tcp_nopush/tcp_nodelayBatch most of a response, flush the final packet immediately
proxy_bufferingBuffer a backend response so a slow client doesn't hold the backend connection open
stub_statusReal-time connection/request counters for quick diagnosis
stub_status is a first stop, not a destination
stub_status's value is its immediacy — a quick, no-setup snapshot of what Nginx is doing right now. Observability's own material covers what comes next: retaining that data over time, alerting when it crosses a threshold, and correlating it against the rest of a system's own metrics — none of which stub_status itself does.
Turning proxy_buffering off is not automatically "faster"
It's tempting to assume disabling buffering removes a layer of overhead and is therefore simply faster. In practice, proxy_buffering off means Nginx passes each chunk of the backend's response straight through to the client as it arrives — if the client is slow to receive data, the connection to the backend stays open for the entire duration of that slow transfer, tying up a backend connection (and the resources behind it) for much longer than buffering would have. The real trade-off is backend resource protection (buffering on) against marginally lower latency for an already-fast client (buffering off) — not a straightforward "off is obviously better" choice.

Hands-On Exercises

Exercise 1

Write the three directives that enable sendfile and the correct paired TCP behavior for serving static files efficiently.

📄 View solution
Exercise 2

A backend response is under heavy load and its connections seem to stay open unusually long whenever a client on a slow mobile connection requests a large file. A teammate suggests turning proxy_buffering off to "speed things up." Using this chapter's own warning box, explain why this is likely to make the actual problem worse, not better.

📄 View solution
Exercise 3

A site is running slowly. Using this chapter's own diagnostic checklist, list the order of checks you'd perform to figure out whether the bottleneck is Nginx itself, the backend, caching, or a connection limit — and briefly explain what each check would tell you.

📄 View solution

Chapter 9 Quick Reference

  • sendfile — kernel sends file data directly; tcp_nopush/tcp_nodelay — batch most of a response, flush the last packet immediately, both on together (not conflicting)
  • proxy_buffering — protects backend connections from slow clients; turning it off shifts that cost onto the backend, not automatically "faster"
  • stub_status — a lightweight, real-time first diagnostic stop; not a substitute for a real observability stack
  • Diagnostic order: error log → stub_status → X-Cache-Status → worker_connections/file-descriptor limits