The AF_XDP Socket and the UMEM: Zero-Copy Packet I/O Between the NIC and User Space

2026-07-24

A normal recv() on a TCP socket touches the packet at least three times: the NIC DMAs it into a driver ring buffer, the softirq pulls it up through the network stack into an sk_buff, and finally copy_to_user() memcpys it into your buffer. Every hop costs cache lines and cycles. AF_XDP throws all of that out. It hands the NIC a giant chunk of your process's memory and lets it DMA packets directly into pages you already have mapped.

The core primitive is the UMEM: a user-allocated region (typically hugepage-backed) registered with the kernel via setsockopt(XDP_UMEM_REG). The UMEM is sliced into fixed-size frames (default 2048 or 4096 bytes). Four single-producer/single-consumer rings coordinate ownership of those frames:

Rings are mmap'd shared memory with a producer index and consumer index — no syscall to enqueue or dequeue. A packet's lifecycle is just an address moving between rings. Zero copies. Zero sk_buff allocation.

An XDP program (BPF, attached at the driver level) decides which flows go to which AF_XDP socket via bpf_redirect_map() into an XSKMAP. Everything else goes up the normal stack. This is how you can run a userspace load balancer on one queue while sshd keeps working on the others.

Concrete example: Cloudflare's l4drop DDoS mitigation uses XDP to drop attack traffic in the driver, but legitimate traffic flowing to their userspace proxy takes the AF_XDP path. On a 100Gbps NIC, they measured ~24 Mpps per core for 64-byte packets — vs. ~2 Mpps through the normal stack. That's a 12× throughput improvement with lower latency, because the packet never touches the softirq path.

Rule of thumb: Each frame in the UMEM costs you memory but saves you a DMA remap. A common sizing is 4096 frames × 2KB = 8MB per RX queue. If your NIC has 16 queues and you want to bind one AF_XDP socket per queue (which RSS demands for scaling), you're committing 128MB just for RX buffers. Use MAP_HUGETLB or you'll blow the TLB reading them.

The gotcha: zero-copy mode requires driver support (XDP_ZEROCOPY). Without it you fall back to XDP_COPY, which memcpys into the UMEM and defeats the entire point. Check with ethtool -i and confirm the driver appears in the kernel's xdp_features list before benchmarking.

Key Takeaway: AF_XDP replaces the kernel network stack with four shared-memory rings and a pre-registered DMA region, letting a single core process tens of millions of packets per second — but only if the driver truly supports zero-copy mode.

All newsletters