The tee() Syscall: Duplicating Pipe Data Without Consuming It

2026-07-11

You know splice() moves data between file descriptors without copying it into user space — the kernel just re-points pipe buffer references. tee() is its lesser-known sibling: it duplicates data between two pipes without consuming the source. After tee(in, out, N, 0), both pipes still contain the N bytes, and neither made a copy — the kernel bumped the refcount on the underlying pipe buffer pages.

The signature: ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags). Both fds must be pipes (this is the constraint that trips people up). The kernel walks the pipe_buffer ring on the input side, and for each buffer it wants to forward, it calls the buffer's ->get() op — which for anonymous pipe pages just increments a page refcount — then links the same struct page* into a new pipe_buffer on the output side.

The zero-copy tee pattern: log every byte flowing through a proxy without touching it.

Both consumers drain independently. The pages get freed when the last pipe_buffer referencing them is released. Zero user-space memory touched the payload, zero read()/write() calls, zero cache pollution from an application-level buffer.

The gotcha: tee() is bounded by whichever pipe has less headroom. Default pipe capacity is 16 pages = 64KB. If your audit consumer (pipe B → log) falls behind, pipe B fills up, tee() returns short or blocks, and now your forward path stalls too — because you can't advance pipe A's read pointer until you've teed those bytes off. Bump both pipes with fcntl(fd, F_SETPIPE_SZ, 1<<20) up to /proc/sys/fs/pipe-max-size.

Rule of thumb: if you're calling read() into a buffer just to write() the same bytes to two destinations, you're paying ~2 syscalls + one memcpy per KB. The splice+tee+splice chain is 3 syscalls total for the whole transfer window, and the memcpy count is zero. At 10 Gbps, that's the difference between saturating the wire and eating a core on memory bandwidth.

The name comes from the Unix tee(1) command, and tee(1) on Linux uses this syscall when its inputs and outputs are both pipes — which is why cmd | tee >(logger) | next is genuinely zero-copy on modern kernels.

See it in action: Check out How to run c program in linux or ubuntu by Akhilesh Reddy to see this theory applied.
Key Takeaway: tee() duplicates pipe contents by bumping page refcounts, letting you fan out a data stream to two consumers without ever copying the bytes into user space.

All newsletters