Posted in Engineering

Zero-Copy I/O Explained: How High-Performance Systems Move Data Without Wasting CPU Cycles

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 solvesUnnecessary copying of data between kernel space and user space during I/O
Core Linux syscallssendfile(2), splice(2), mmap(2); newer workloads also use io_uring
Typical savingsEliminates the application-level copy steps (OS cache → app buffer → socket buffer) on the read/transfer path
PlatformLinux-specific syscalls (sendfile/splice); conceptually available on other Unix-likes with different APIs
Well-known adoptersApache Kafka, Netty, Nginx
Main trade-offLess 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 + writeZero-copy (sendfile)
Disk → OS page cacheYesYes
OS cache → application bufferYes (extra copy)Skipped
Application buffer → socket bufferYes (extra copy)Skipped
OS cache / socket buffer → NICYesYes, directly
Application touches the bytesYesNo

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 FileRegion abstraction 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() and splice() 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 with write().
  • 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


Discover more from Software Online Review

Subscribe to get the latest posts sent to your email.

Unknown's avatar

Author:

Innovations, logic & design thinking