The Kernel Splice Syscall and Pipe Buffers: Zero-Copy I/O Between File Descriptors

2026-07-10

When you read() from a socket and write() to a file, the bytes make a round trip through userspace: kernel → user buffer → kernel → disk. Two copies, two syscalls, and your L1 cache gets trashed by data you never actually look at. splice() is the kernel's answer: move data between two file descriptors without ever mapping it into your address space.

The trick is that splice doesn't move bytes — it moves pipe buffer references. A Linux pipe is not a byte stream; it's a ring of up to 16 struct pipe_buffer entries, each pointing to a page (or fragment) of kernel memory with an offset and length. When you splice from a socket into a pipe, the kernel takes the socket's sk_buff pages and hands their references to the pipe. When you splice from the pipe to a file, the filesystem reads directly from those same pages. Zero copies. The data never leaves kernel space.

The catch: one end of every splice() must be a pipe. You cannot splice socket→file directly — you need a pipe as the intermediary. This is why high-performance proxies like HAProxy and nginx allocate a pipe per connection just to shovel bytes through it:

Real-world example: nginx's sendfile directive uses splice under the hood for HTTP file serving. Benchmark on a 10Gbps link serving a 1GB file: read()+write() loop pegs one CPU at 100% pushing ~3 GB/s. Splice does the same workload at ~30% CPU — saturating the NIC. The wins come from (1) no user-space copy, (2) no page fault on the user buffer, (3) no cache pollution — the CPU never touches the payload.

The pipe buffer limit rule of thumb: a pipe holds 16 pages = 64KB by default (raise with fcntl(F_SETPIPE_SZ) up to /proc/sys/fs/pipe-max-size, usually 1MB). If your splice source produces faster than the sink can drain, the pipe fills and splice blocks. So splice's throughput ceiling per pipe is pipe_size / round_trip_latency. A 1MB pipe with a 10µs sink latency gives you 100 GB/s of headroom — plenty. A 64KB pipe with 1ms disk writes gives you 64 MB/s — often the actual bottleneck.

Gotcha: SPLICE_F_MOVE is advisory — the kernel silently falls back to copying if the source pages can't be stolen (e.g., they're still referenced elsewhere, or the filesystem doesn't support page stealing). Check /proc/<pid>/io to verify you're actually zero-copy.

See it in action: Check out How to Use sendfile() and splice() for Zero Copy Network I/O – Boost Throughput
amp; Cut CPU Load by SystemDR - Scalable System Design to see this theory applied.
Key Takeaway: splice() moves page references through a pipe instead of copying bytes through userspace, but one end must always be a pipe — which is why every zero-copy proxy allocates one per connection.