With nginx sendfile and tcp_nopush I deliver static files using zero-copy from the file system to the socket, thereby noticeably reducing both CPU load and the number of packets. When configured correctly, both directives improve transmission efficiency, reduce overhead, and lay the foundation for proper nginx optimization of assets and downloads.
Key points
- Zero-Copy Using sendfile: fewer copies, higher throughput
- tcp_nopush Buffers packets: larger frames, less overhead
- Combination counts: sendfile + tcp_nopush + tcp_nodelay
- Use cases Prioritize: static assets, large downloads
- Tests For NFS/SMB: Measure the impact; if necessary, disable sendfile
Why sendfile Unlocks So Much Performance for NGINX
I activate sendfile, because the kernel can send files directly through the network stack without the need for additional copy operations in user space. This zero-copy path reduces context switches and saves CPU cycles, especially when many concurrent clients are retrieving static content. Large files such as images, CSS, JavaScript, or archives benefit because data transfer is smoother and involves less overhead. System caches also operate more efficiently, as there are fewer memory movements and the kernel controls the data’s path. The benefits are most evident on local file systems, which is why I measure there first before applying the results to more exotic setups.
What exactly tcp_nopush does and when it really shines
With tcp_nopush I instruct the system to send TCP packets only when they are sufficiently filled, rather than sending small segments too early. Under Linux, this corresponds to TCP_CORK; under FreeBSD, to TCP_NOPUSH; and in both cases, the number of packets decreases measurably. This directive does not minimize latency; rather, it aims to achieve a better balance between payload and overhead. I specifically use `tcp_nopush` for static files because that’s where contiguous data streams yield the greatest efficiency gains. Without `sendfile`, `tcp_nopush` has no effect, so I always enable both settings together.
`sendfile` and `tcp_nopush` as a pair: this is how I set the foundation
The combination of sendfile and tcp_nopush reduces copying and bundles packets, allowing a server to handle significantly more parallel transfers per CPU core. I configure both at the HTTP context level and often add tcp_nodelay so that the last remnants of a flow can be sent without delay. It remains important to test with real traffic, as packet sizes, MTU, and clients vary, and the optimal balance may differ slightly depending on the workload. For static directories, global activation is usually sufficient, while for dynamic response routes, I pay close attention to the impact. This combination provides a solid foundation for further nginx optimization steps that will be added later.
| directive | Purpose | Typical effects | Dependence |
|---|---|---|---|
| sendfile on | Zero-Copy from File to Socket | Lower CPU load, higher throughput | Local file system is ideal |
| tcp_nopush on | Fill Packages, Reduce Overhead | Fewer segments per file | Works only with `sendfile` |
| tcp_nodelay on | Send the last bytes without waiting | Prompt Completion of the Transfer | Added tcp_nopush |
This is how tcp_nodelay works in conjunction with tcp_nopush
I activate tcp_nopush, to send the beginning of a transfer in larger packets, and enable tcp_nodelay at the same time so that the completion doesn't get stuck. Both settings affect different phases of the flow and do not interfere with each other when NGINX delivers files via sendfile. Especially with many small files, `tcp_nodelay` prevents the client from waiting unnecessarily due to small amounts of remaining data. I first test the combination in staging, monitor RTTs and segment sizes, and compare them with live metrics. This ensures efficiency at the beginning and speed at the end of the transfer.
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
}
Typical use cases: where the directives have a strong impact
For large Downloads For files such as videos, archives, or ISO images, the kernel’s zero-copy path significantly reduces CPU time per transfer. In CDN-like setups with many CSS, JS, and font files, `tcp_nopush` saves segments, thereby increasing the usable bandwidth per socket. On well-cached WordPress sites, most requests are for static assets, which is why I see the effect there very quickly. Build artifacts, container images, and installers also benefit, provided they’re stored locally and don’t come via an unreliable network file system. If you’re expecting traffic spikes, this combination will help you get the most stability out of your existing hardware.
Practical Example: NGINX for WordPress with Caching and Assets
In WordPress setups, I set sendfile, set `tcp_nopush` and `tcp_nodelay` globally, serve static resources directly, and keep PHP-FPM strictly separate for dynamic paths. I add appropriate cache headers for images, CSS, and JavaScript so that browsers make fewer round trips. When delivering streaming-style responses, I consider how they interact with buffering and test how chunk sizes affect latency and throughput; the overview of Response streaming in chunks. For text-based content, I use compression without unnecessarily compressing binary files. This keeps the request flow stable, reduces CPU load, and keeps the time to first byte short.
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
gzip on;
gzip_types text/css application/javascript image/svg+xml;
server {
listen 80;
server_name blog.example.com;
root /var/www/blog;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
}
}
When I deliberately disable sendfile
I switch sendfile when files are stored on NFS, SMB, or distributed file systems, which delivered lower throughput in my test. Certain drivers or latencies in the storage path can negate the zero-copy advantage, which is why measurements are key. When encountering sporadic network quirks, I first disable `tcp_nopush` to isolate the effects before investigating `sendfile` itself. Unusual kernel bugs or older stacks can also be reasons to temporarily switch to the classic read-write path. It remains important to implement changes incrementally and to back them up with metrics.
Sources of error that I keep an eye on
I first check whether tcp_nopush is accidentally enabled while `sendfile` remains disabled, because in that case the setting has no effect. For dynamic paths, I monitor whether additional buffering increases latency and weigh the benefits against the response time. On high-latency networks, I measure whether larger packets actually help or if I need to fine-tune segment sizes and keep-alive settings. The MTU configuration and offloading features of the network card can also noticeably affect the results. Clean logs, PCAP samples, and correlated system metrics quickly show me where to make adjustments.
Taking a Holistic Approach to NGINX Performance: Additional Adjustment Options
Besides sendfile Setting the correct number of `worker_processes` and `worker_connections` pays off, so I don’t have to artificially limit sockets. On Linux, I use epoll and ensure there are enough file descriptors so that traffic spikes don’t cause bottlenecks. For text content, I enable gzip or Brotli and test whether the compression level places a reasonable load on the CPU. At the transport layer, I keep connections open longer and optimize keep-alive, as described in the guide Keep-Alive Tuning provides practical guidance. TLS, session reuse, and HTTP/2 or HTTP/3 round out the setup and support high concurrency with moderate latency.
Limitations and Special Cases: TLS, HTTP/2/3, and Proxying
I take into account that sendfile technically applies only to unencrypted file paths or specific kernel functions. With classic TLS, NGINX encrypts the bytes in user space, which eliminates the zero-copy benefit; modern kernels can partially offload encryption to the kernel, which restores the benefit, but this is not available in every setup. In HTTP/2 If data is contained in frames, multiple responses share a single TCP connection, and NGINX actively reorders the bytes—sendfile is less relevant in this scenario. HTTP/3 It is based on UDP/QUIC and follows different rules, so I achieve efficiency gains primarily through buffering, congestion control, and appropriately chosen chunk sizes. As Reverse proxy sendfile only works if I'm actually serving files from the local file system; responses from proxy_pass or fastcgi_pass They pass through user space anyway. That's why I strictly separate assets from the dynamic path, so that the zero-copy approach is utilized to the fullest.
Understanding Compression: gzip/Brotli vs. gzip_static
Whenever NGINX compresses content on the fly, it must read the file, process it, and write the result—and in the process, it loses sendfile its advantage. For static assets, I therefore use, whenever possible, pre-compressed files (e.g., .gz or .br) and have them served directly. This preserves the zero-copy path because NGINX can pass the pre-compressed file just like any other asset. For text-heavy, rarely updated content, this allows me to achieve CPU savings and stable throughput without sacrificing transfer time. For binary files and already compressed formats, I avoid any runtime compression—here, pure I/O throughput is what counts, and `sendfile` plus `tcp_nopush` really shine.
AIO, directio, and Page Cache: Patterns for Small and Large Files
I combine sendfile using asynchronous I/O and direct disk access to achieve optimal performance depending on the file size. Small to medium-sized files benefit from the kernel's page cache and remain on the `sendfile` path. Very large files, on the other hand, can displace the cache; in that case, I read them specifically using directio outside the cache and use AIO threads. This reduces the load on memory and keeps latency low for other requests. A typical pattern looks like this:
http {
# Default path: Zero-copy from the page cache
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Large files: Bypass the cache and read asynchronously
aio threads;
directio 4m; # applies only to files >= 4 MiB
output_buffers 1 512k; # buffers for directio paths
sendfile_max_chunk 1m; # fairness under high load
}
With this scaling, small assets remain extremely efficient, while very large transfers do not flood the memory. Important: directio disables the sendfile path for affected files—which is exactly what I intend for the large-file use case.
Fairness and Flow Control Under Load
During periods of high load, I want to avoid having a single stream monopolize the CPU or the socket. I set sendfile_max_chunk, so that NGINX returns the kernel after a defined number of bytes and leaves room for other connections. For bandwidth throttling, the following are helpful limit_rate and limit_rate_after, for example, to throttle bulk downloads while keeping UI assets fast. With postpone_output I control the response size at which NGINX begins sending—in combination with `tcp_nopush`, this ensures clean packet segmentation. In addition, I pay attention to lingering_close, so that any remaining packets can be transmitted properly and the socket is not abruptly terminated.
File Systems, Read-Ahead, and Storage Paths
Because sendfile When using page caches, the underlying file system plays a major role. I check the read-ahead values and adjust them so that sequential reads from large files don't stall, without crowding out smaller assets. On ext4 or xfs I observe how well prefetching and the I/O scheduler align with my throughput pattern. On network file systems (NFS/SMB), I rigorously test rsize/wsize, caching, and latencies, because even small deviations can neutralize the zero-copy advantage. My rule remains: first, maximize local paths; then, carefully adjust external stacks—and always prioritize measured values over gut feelings.
Pragmatically Tuning the Network Stack and NIC Offloading
For high connection counts, I rely on the automatic buffer adjustment of modern stacks, but I adjust the transmit and receive buffers as needed. NIC offloads such as TSO, GSO, and GRO noticeably reduce CPU load; however, I exercise caution when conducting measurements, as packet captures can be distorted by offloading (resulting in what appears to be a small number of very large segments). I therefore correlate pcap‑Traces with metrics from NGINX and the kernel to distinguish actual wire-level values from offload artifacts. For latency spikes, I briefly pause tests with offloads disabled, document the difference, and then decide which approach provides more benefit in continuous operation.
Configuration Templates by Location: Enable and Disable as Needed
I'm keeping my options open, sendfile to override depending on the path or file type. For static directories, it remains enabled; for streaming or dynamic paths, I selectively disable it when buffers or filters (e.g., compression) take precedence. Here's a quick example:
server {
listen 80;
server_name static.example.com;
root /var/www/static;
# Static Assets: Zero-Copy
location /assets/ {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
expires 7d;
}
# Dynamic or Streaming: Flexibility Over Zero-Copy
location /api/ {
sendfile off;
proxy_pass http://app_upstream;
}
}
This separation prevents me from losing benefits on one side simply because another path has specific requirements.
Range, Slices, and Large Catalogs
For large objects, play range-Requests play to their strengths: The client loads only the necessary parts, and connections remain stable. In content catalogs with very large files, I like to segment transfers logically—the server load is distributed more evenly, and errors such as timeouts cost less time. In caching scenarios, I prevent „thundering herds“ by buffering responses sensibly, but I don’t artificially hold onto small chunks or remaining data. Interaction with `tcp_nopush` remains key here: I keep initial segments large, but I don’t let the end wait.
Measurement and Testing Strategy: Reliably Demonstrating Effects
I validate optimizations with reproducible tests. On the server side, I monitor CPU profiles, $request_time, $bytes_sent, active connections, and context switches. On the network, I measure segment sizes, retransmissions, and RTT distribution; I correlate packet captures with socket statistics to account for offload effects. On the client side, I compare TTFB, First Contentful Paint, and download times under realistic RTTs and bandwidths. I vary the MTU, keep-alive settings, and file sizes so that I don’t just see best-case curves. In the end, I use hard data to determine whether `sendfile` or `tcp_nopush` delivers the desired stability and efficiency for the respective workload—and I fine-tune the settings until they do.
HTTP Details That Make All the Difference: Range and Streaming
I use range-Requests for large files, so that clients only reload the parts they need and connections remain stable. Especially when it comes to video seek operations and update resumptions, proper support for byte ranges helps distribute throughput effectively; background information is available on the page at HTTP Range Requests. For continuous responses with growing bodies, I test streaming strategies and ensure that buffers do not inadvertently hold data for too long. In doing so, I respect caches and set appropriate headers so that proxies and browsers behave correctly. I take the interaction with `tcp_nopush` into account because packet sizes and the timing of the flush directly affect perceived speed.
Briefly summarized
With sendfile I efficiently forward files directly to the kernel, and with `tcp_nopush`, I ensure the packets are filled properly before they burden the connection. These two directives complement each other, while `tcp_nodelay` delivers the last remaining byte without delay. I test the effect under real-world traffic, paying attention to the storage path, MTU, keep-alive, and compression, and I measure consistently. For WordPress and CDN-like workloads, the benefits become apparent particularly quickly because many requests are for static assets. Those who apply these settings strategically can achieve higher throughput per core, reduce overhead, and create headroom for real growth spikes.


