Every time a web server streams a file, a message broker ships a log segment, or a CDN node serves a cached asset, the operating system has a choice: copy that data through the application’s own memory, or hand it off inside the kernel and skip the detour. The second option is “zero-copy” I/O, and it’s one of the quieter reasons systems like Kafka, Nginx, and Netty can push huge amounts of data without burning CPU cycles on work that never needed to happen.
At a Glance
| What it solves | Unnecessary copying of data between kernel space and user space during I/O |
| Core Linux syscalls | sendfile(2), splice(2), mmap(2); newer workloads also use io_uring |
| Typical savings | Eliminates the application-level copy steps (OS cache → app buffer → socket buffer) on the read/transfer path |
| Platform | Linux-specific syscalls (sendfile/splice); conceptually available on other Unix-likes with different APIs |
| Well-known adopters | Apache Kafka, Netty, Nginx |
| Main trade-off | Less flexibility — the data can’t be inspected or transformed by the application mid-transfer |
The Problem: What a “Normal” Copy Actually Costs
A naive file-to-network transfer — read a file, then write it to a socket — looks cheap in code but isn’t cheap in hardware. As Kafka’s own design documentation and independent breakdowns of its internals describe, a conventional path pulls data from disk into the OS page cache, then copies it again into the application’s buffer, then copies it a third time into the socket buffer the kernel uses for the network stack, before the network card finally sends it. Each of those hops crosses the boundary between kernel space and user space, and each one costs CPU time and memory bandwidth to move bytes that the application never actually needed to touch — it was just a pass-through.
This matters most for high-throughput, pass-through workloads: a message broker relaying log segments to consumers, a static file server, or a proxy forwarding a response body. In all of these, the application doesn’t need to read or modify the bytes — it just needs to move them from point A to point B as fast as possible.
How Zero-Copy Works
Zero-copy techniques let the kernel move data directly — disk cache to network card, or one file descriptor to another — without routing it through a user-space buffer at all. The core Linux mechanism is the sendfile(2) system call:
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
According to its Linux manual page, sendfile() “copies data between one file descriptor and another,” and because the data never travels to user space and back, it is more efficient than a matching pair of read(2) and write(2) calls. A related call, splice(2), moves data between two file descriptors where at least one is a pipe — its man page is explicit that the point is to move data “without copying between kernel address space and user address space,” by passing around reference-counted pointers to kernel memory pages rather than the page contents themselves. mmap(2) offers a third route: mapping a file directly into a process’s address space so reads happen via memory access instead of an explicit copying syscall. More recently, Linux’s io_uring interface has given low-level network and storage code another way to cut syscall and copy overhead, though it’s a broader asynchronous I/O framework rather than a drop-in replacement for sendfile().
| Traditional read + write | Zero-copy (sendfile) | |
| Disk → OS page cache | Yes | Yes |
| OS cache → application buffer | Yes (extra copy) | Skipped |
| Application buffer → socket buffer | Yes (extra copy) | Skipped |
| OS cache / socket buffer → NIC | Yes | Yes, directly |
| Application touches the bytes | Yes | No |
That table reflects the walkthrough popularized by engineering explainers of Kafka’s design: in the traditional path, data makes extra stops at the application layer and the socket buffer before reaching the network card; with sendfile(), the OS page cache hands data to the network card directly, and the broker process never has to allocate a buffer for it or touch it at all.
Real-World Uses
- Apache Kafka — Kafka brokers lean on zero-copy transfer to move log segments from disk to consumers without the broker process copying the bytes through its own heap, which is a documented part of why a single broker can sustain very high read throughput.
- Netty — Netty’s
FileRegionabstraction represents a region of a file sent through a channel, and Netty’s own API documentation describes it as supporting zero-copy file transfer; it’s the mechanism behind efficient file-serving code built on Netty (which underlies parts of gRPC and many Java web servers). - Nginx — Nginx exposes a
sendfile on;directive specifically so static file responses can be served via the kernel’s zero-copy path instead of being read into a worker’s buffer and written back out.
Trade-Offs and Limitations
- No mid-flight processing. Because the application never sees the bytes, zero-copy only works for pure pass-through transfers — not for workloads that need to compress, encrypt, parse, or transform the data first.
- Platform-specific.
sendfile()andsplice()are Linux syscalls with their own constraints (for example,sendfile()‘s input descriptor has historically needed to support mmap-like access, so it can’t be a socket); the man pages themselves warn against relying on sendfile’s exact semantics in portable code. - Partial transfers still happen. A single
sendfile()call can transfer fewer bytes than requested, so correct code has to handle looping until the transfer completes, same as withwrite(). - Not always a true zero-copy in practice. The
splice(2)man page itself notes that actual page copies can still occur when the kernel can’t just move pages around — for instance when buffers don’t align to full pages.
Bottom Line
Zero-copy I/O isn’t a trick or a benchmark gimmick — it’s a direct consequence of recognizing that a lot of server-side work is pure data relay, and that every unnecessary copy between kernel and user space costs real CPU time and memory bandwidth at scale. For teams building anything that moves large volumes of bytes — a broker, a proxy, a file server — knowing that sendfile(), splice(), and friends exist (and why Kafka, Nginx, and Netty already use them) is a useful piece of systems literacy, even if you never call the syscall directly yourself.
FAQ
Is zero-copy I/O only relevant to C/systems programming?
No. Most developers encounter it indirectly — through a sendfile on; setting in Nginx, Kafka’s broker-side design, or a Java NIO/Netty file-transfer API — without ever calling the underlying syscall themselves.
Does zero-copy mean literally no copying happens anywhere?
No. Data still moves from disk into the OS page cache, and from there to the network card. What’s eliminated are the extra copies into and out of the application’s own user-space buffer.
Can I use sendfile() if I need to encrypt or compress the data first (e.g. TLS)?
Not directly — since the application never sees the bytes, it can’t transform them. That’s why plain zero-copy static file serving and TLS-terminated responses are handled by different code paths; some systems use kernel TLS (kTLS) offload to regain some of the benefit even with encryption, but that’s a separate mechanism from the plain sendfile/splice path described here.
Sources
- sendfile(2) — Linux manual page
- splice(2) — Linux manual page
- ByteByteGo: Why Is Kafka Fast?
- Arpit Bhayani: Zero-copy writes make Kafka fast, but how exactly?
- Netty FileRegion API documentation
Discover more from Software Online Review
Subscribe to get the latest posts sent to your email.
