The MSG_ZEROCOPY Flag: How send() Skips the User-to-Kernel Copy at the Price of an Asynchronous Completion

2026-07-19

A normal send() copies your buffer into a fresh kernel sk_buff. That copy is fine for a 1500-byte packet, but for a 1 MB TLS record it burns memcpy bandwidth and pollutes the L2 cache. MSG_ZEROCOPY (Linux 4.14+) lets the kernel pin your user pages and reference them directly from the skb's fragment array — no copy, ever.

The catch is you don't own the buffer anymore. The NIC may DMA from it minutes later (retransmits, TCP small queues, qdisc backlog). Reusing or freeing it would corrupt the wire. So the kernel returns a completion notification asynchronously via the socket's error queue, told to you as an SO_EE_ORIGIN_ZEROCOPY message.

The protocol:

Concrete example — an HTTPS proxy sending large TLS records: a naive send(4 MB) costs ~1.3 ms of pure memcpy at 3 GB/s. With MSG_ZEROCOPY it costs a page-pin walk (get_user_pages_fast) plus the error-queue drain — measured on production kernels at roughly ~40% CPU savings above 10 KB payloads, but ~5% worse below 4 KB because the notification bookkeeping outweighs the saved copy.

Rule of thumb: only enable MSG_ZEROCOPY when your average send is larger than two pages (~8 KB). Below that, the notification overhead and the extra syscall to drain the error queue dominate.

Sharp edges:

Key Takeaway: MSG_ZEROCOPY trades a synchronous memcpy for asynchronous page pinning plus an error-queue notification — a clear win above ~8 KB per send, and a net loss below it.

All newsletters