Skip to main content
José David Baena

On this page

AF_XDP and DPDK Win by Moving the Kernel Boundary

Banner.jpeg
Published on
/21 mins read

You already use XDP, Linux's early packet-processing hook, to drop obvious junk before it reaches the rest of the stack. One traffic class still burns cores, and the next design review turns into AF_XDP, Linux's queue-bound packet socket, versus the Data Plane Development Kit (DPDK), a userspace packet-processing framework. Then somebody asks the question that actually matters: who owns packet buffers now, who still owns transport, and what proves memory is safe to reuse?

Episodes one, two, and three all circled the same rule: an owner decides when storage may be read, mutated, reused, or freed, and a completion proves when the previous owner can let go.

TL;DR: AF_XDP keeps Linux in charge of the network device, XDP policy, and much of queue life while lending selected frames to user space through registered memory and rings. DPDK moves queue polling, packet-buffer lifetime, and much more of the transport job into user space. DPDK's AF_XDP poll mode driver (PMD) is a DPDK sub-mode that still sits on AF_XDP underneath, not a fourth philosophy. Copy mode versus zero-copy mode, driver support, memory locality, and rollback cost usually decide the right choice before a throughput chart does.

The version line matters here. I checked the kernel-facing claims against Linux 7.1 and the DPDK path against DPDK 26.07. Driver support remains a runtime fact, so neither version number proves that your NIC entered zero-copy mode.

AF_XDP and DPDK win by moving the kernel boundary to different places, not by erasing it.

Moving the boundary explains the speedup better than "kernel bypass"

Kernel bypass sounds decisive and hides the interesting part. The useful comparison is between three different boundaries: kernel sockets, where Linux owns packet memory and transport semantics end to end; AF_XDP, where Linux still owns the device and queue machinery but hands selected frames to user space; and DPDK, where a PMD in user space polls queues directly and owns far more of the hot path.

That difference matters because the data plane is not the whole system. A queue is one RX or TX work stream on a network interface card (NIC). A transport contract is the bundle of ordering, congestion control, retransmission, visibility, and failure behavior your application inherits from the stack. Linux 7.1 documents AF_XDP's queue-bound sockets, UMEM, and ring handoff, while DPDK 26.07 documents ports, queues, and userspace polling APIs. Those are not the same kind of move.

Boundary moveKernel socketsAF_XDPDPDK
Packet memory ownerkernel skb and socket buffersUMEM frames on one bound queue, copied or direct by modembufs from a user mempool
Polling ownerIRQ + NAPI in kernelkernel NAPI + user ring pollinguser-space PMD loop
Transport ownerkernel TCP, TLS, conntrackapplication owns packet semantics above the frameapplication or user-space stack
Safe reuse proofsocket API completion rulesFILL/COMPLETION ring disciplinembuf free or TX reclaim
Best first questionNeed stream semantics?Need selective packet ownership?Need appliance-style dataplane control?

That table turns a slogan into a review checklist. If your service still needs kernel TCP, TLS, conntrack, or ordinary socket tooling, the left column stays real. If you only need selective queue-bound packet ownership, AF_XDP may be enough. If you want appliance-style control over queues, burst loops, and protocol implementation, DPDK earns its extra operational surface.

AF_XDP works because Linux lends one queue's frames through UMEM and four rings

In AF_XDP, Linux binds one socket to one netdev queue and exposes packet delivery through a shared memory contract rather than a stream API. UMEM is a region of registered user memory split into fixed-size frames. A ring here is a single-producer, single-consumer circular descriptor queue. Linux 7.1 defines four rings: the FILL ring for free frame addresses from user space to the kernel, the RX ring for received packet descriptors from the kernel to user space, the TX ring for transmit descriptors in the other direction, and the COMPLETION ring for transmit-safe-to-reuse notifications back to user space.

The key word is selective. An XDP program can still PASS, DROP, or REDIRECT traffic, and AF_XDP redirection happens through XSKMAP. That means you can keep most traffic on ordinary Linux networking while steering only one flow class or one queue into user space. Linux 7.1's UAPI makes queue binding explicit, and its xdpsock_user.c sample shows the practical ring lifecycle.

The startup path is where a lot of AF_XDP writeups get sloppy. If zero-copy is non-negotiable, force it and accept bind failure when the driver cannot do it. If fallback is fine, do not assume bind success means zero-copy happened. The Linux 7.1 docs describe default fallback behavior, and the UAPI exposes XDP_OPTIONS_ZEROCOPY so you can print the actual mode.

SEC("xdp")
int steer_to_xsk(struct xdp_md *ctx)
{
  return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_PASS);   // ①
}
 
/* Pattern A: zero-copy is mandatory, so fail fast. */
struct xsk_socket_config force_zc = {
  .xdp_flags = XDP_FLAGS_DRV_MODE,
  .bind_flags = XDP_ZEROCOPY | XDP_USE_NEED_WAKEUP,                    // ②
};
 
ret = xsk_socket__create(&force_xsk, ifname, queue_id, force_umem,
                         &force_rx, &force_tx, &force_zc);
if (ret) {                                                             // ③
  fprintf(stderr, "AF_XDP zero-copy unavailable on %s q%u: %s\n",
          ifname, queue_id, strerror(-ret));
  return ret;
}
 
/* Pattern B: best effort, then log what you actually got. */
struct xsk_socket_config best_effort = {
  .xdp_flags = XDP_FLAGS_DRV_MODE,
  .bind_flags = XDP_USE_NEED_WAKEUP,
};
 
ret = xsk_socket__create(&run_xsk, ifname, queue_id, run_umem,
                         &run_rx, &run_tx, &best_effort);
if (ret)
  return ret;
 
struct xdp_options opts = {};
socklen_t optlen = sizeof(opts);
 
ret = getsockopt(xsk_socket__fd(run_xsk), SOL_XDP, XDP_OPTIONS,
                 &opts, &optlen);                                     // ④
if (ret != 0) {
  int saved_errno = errno;
  fprintf(stderr, "AF_XDP mode: unknown; XDP_OPTIONS failed: %s\n",
          strerror(saved_errno));
  return -saved_errno;
}
if (optlen < sizeof(opts)) {
  fprintf(stderr, "AF_XDP mode: unknown; short XDP_OPTIONS response\n");
  return -EOPNOTSUPP;
}
 
if (opts.flags & XDP_OPTIONS_ZEROCOPY)
  fprintf(stderr, "AF_XDP mode: zero-copy\n");
else
  fprintf(stderr, "AF_XDP mode: copy\n");

① The redirect key is the RX queue index. A packet only reaches the XSK bound to that queue, which is why RSS and queue steering decide who can receive the frame.

XDP_ZEROCOPY forces direct UMEM use instead of silent fallback. XDP_USE_NEED_WAKEUP exposes when user space must wake the kernel side, which Linux 7.1 recommends for performance.

③ Forced zero-copy should fail loudly on unsupported drivers. That is a feature, not a nuisance, when the benchmark or product requirement really depends on zero-copy.

④ Best-effort mode must query XDP_OPTIONS after bind. A failed query is unknown, not copy mode; so is a response too short to contain struct xdp_options. This example stops instead of producing a false result. Only a complete, successful query can distinguish XDP_OPTIONS_ZEROCOPY from copy mode.

The ownership path is precise once you draw it. RX, copy mode: NIC DMA lands in kernel-managed RX memory, XDP redirects, Linux copies bytes into a UMEM frame, the RX ring publishes a descriptor, your app reads the frame, and only then may it return that frame address on the FILL ring. RX, zero-copy mode: the queue uses UMEM-backed frames directly, the RX ring publishes descriptors for those frames, and your app still returns addresses through the FILL ring when done. TX, both modes: your app writes into a UMEM frame, pushes a descriptor on the TX ring, Linux and the NIC transmit it, and the COMPLETION ring tells you the frame is safe to reuse.

That is why AF_XDP is not a fast stream socket. It is a queue-bound frame handoff API. Shared UMEM exists across queues and even devices, but the same docs are explicit that FILL and COMPLETION are single-producer, single-consumer (SPSC) rings. If you ignore the SPSC part, you did not build a clever shared-UMEM design. You built a race. The UAPI also includes XDP_USE_SG for scatter-gather, where one logical packet spans multiple buffers, but that remains driver- and NIC-dependent in practice the Linux 7.1 UAPI lists the flag and DPDK 26.07 treats support as capability-dependent. For your service, this usually means AF_XDP fits best when one UDP, QUIC, telemetry, or filtering path needs packet ownership and the rest of the host should keep acting like Linux.

DPDK moves the boundary farther because user space owns polling and packet buffers

DPDK moves the line farther out. Linux still helps with hugepage reservation, device assignment, and protection setup, but the hot path becomes a user-space dataplane built around PMDs, burst polling, and application-owned packet buffers. An mbuf is DPDK's packet metadata object plus a reference to the data buffer. A mempool is the allocator that recycles those buffers. DPDK 26.07 defines those contracts in the ethdev, mbuf, and mempool guides.

That design brings real obligations with it. A hugepage is a larger virtual memory page used to reduce mapping overhead and support DMA-friendly pools. VFIO is Linux's secure device-assignment framework, and an I/O memory management unit (IOMMU) is the hardware address-translation layer that constrains device DMA. DPDK 26.07 documents hugepage and NUMA requirements and VFIO, UIO, and device-binding choices. Those are not tuning footnotes. They are part of the architecture.

One subtle hybrid is worth naming early. DPDK 26.07's AF_XDP PMD still gives you a DPDK application shape and mbuf API, but the queue contract underneath is AF_XDP, not a separate fourth model. The DPDK AF_XDP datapath guide makes the same layering explicit. I treat it as a DPDK sub-mode for that reason.

uint16_t n = rte_eth_rx_burst(port_id, queue_id, pkts, BURST);         // ①
for (uint16_t i = 0; i < n; i++)
  handle_packet(pkts[i]);                                              // ②
 
uint16_t sent = rte_eth_tx_burst(port_id, queue_id, pkts, n);          // ③
for (uint16_t i = sent; i < n; i++)
  rte_pktmbuf_free(pkts[i]);                                           // ④

rte_eth_rx_burst() returns pointers to mbufs whose data buffers were already prepared for DMA and filled by the NIC. No kernel socket object appears in the hot path.

② Your application now owns parse, scheduling, backpressure, and often protocol behavior for those packets.

③ Accepted mbufs move into TX queue and NIC ownership until transmit cleanup reclaims them.

④ Unsent mbufs remain your problem. You must retry or free them, or you leak packet memory and lie to your benchmark.

The ownership path here is also explicit. The mempool pre-populates RX descriptors with DMA-mappable buffers. The NIC writes packet bytes directly into those buffers by direct memory access (DMA), device-driven memory access without a CPU copy loop. The PMD polls descriptors from user space, returns mbuf pointers to the app, and the app either frees them back to the mempool or queues them for transmit. That is why DPDK often wins on dedicated dataplane boxes and why it costs more to operate. The same choice that removes kernel involvement also makes your process responsible for far more correctness, visibility, and recovery work.

That hybrid AF_XDP PMD also inherits AF_XDP's wakeup and busy-poll reality. A busy-poll setup keeps the CPU spinning for packets instead of sleeping for interrupts. DPDK 26.07 enables it by default on sufficiently new kernels and recommends napi_defer_hard_irqs plus gro_flush_timeout. Linux 7.1's NAPI docs explain the latency-versus-CPU trade-off, and Suricata 8.0.6 shows the same knobs from an operator's angle. If those prerequisites are missing, you are not measuring the same DPDK AF_XDP PMD path people cite in low-latency talks.

"Zero-copy" only means something after you prove the mode and the reuse event

The first branch that matters is not AF_XDP versus DPDK. It is copy mode versus zero-copy mode, plus the event that releases memory safely. AF_XDP supports both modes under one API. By default, the bind path attempts zero-copy and can fall back to copy mode. If you force XDP_ZEROCOPY, bind fails when the driver cannot supply it. Linux 7.1's UAPI exposes XDP_OPTIONS_ZEROCOPY, so your app can and should print the actual result at startup.

A lot of bad benchmark charts start here. Open vSwitch exposes best-effort, native, native-with-zerocopy, and generic modes because deployers hit different driver realities. DPDK has a different truth problem. It usually avoids the kernel socket copy, but that does not mean “no bytes move.” The NIC still performs DMA, the CPU still pays for cache fills and control traffic, and transmit still needs a reclamation point before the app can recycle storage. DPDK 26.07's AF_XDP PMD can force copy mode with force_copy; without it, the PMD still depends on the underlying AF_XDP and driver path per the current guide. DPDK on the outside does not make AF_XDP fallback disappear underneath.

PathRX byte pathSafe reuse proofWhat to log at startup
Kernel sockets + XDPkernel RX memory -> socket APIsocket semanticsXDP mode and queue layout
AF_XDP copy modekernel RX/page_pool -> copy -> UMEMreturn frame to FILL; TX via COMPLETIONactual mode, driver, queue, MTU
AF_XDP zero-copy modeNIC DMA -> UMEMsame ring disciplineXDP_OPTIONS_ZEROCOPY, driver, queue
DPDK native PMDNIC DMA -> mbuf data bufferrte_pktmbuf_free() or TX reclaimPMD, hugepage layout, queue/core map

page_pool is Linux's queue-local RX memory recycler. It explains why receive-side zero-copy is a lifecycle problem, not just an API brand. Episode one framed that as an ownership ledger. Episode three showed the same thing on kernel-managed receive paths. The packet APIs differ, but the safe-reuse question doesn't.

Queue placement and NUMA usually decide the result before the API name does

Once the design is correct, receive-side scaling (RSS), the NIC's hash-based receive steering, and Non-Uniform Memory Access (NUMA), the machine's non-uniform memory layout, usually move the tail harder than the packet API brand. Linux 7.1 explains RSS, RPS, and XPS, its NAPI docs explain interrupt-moderated polling, and its sysctl docs cover busy-poll controls. AF_XDP still lives in that world; need_wakeup is the tell. DPDK normalizes explicit busy-spin loops instead.

Bursting changes the comparison too. DPDK 26.07 states plainly that larger bursts amortize queue and MMIO work but can raise latency, while burst size one often lowers latency at a throughput cost. So if one graph uses AF_XDP with wakeups and another uses DPDK with dedicated spinning cores and a different burst size, the chart is not comparing APIs. It is comparing policies.

ethtool -L eth0 combined 4                                 # ①
ethtool -X eth0 equal 4                                    # ②
grep -E 'eth0|CPU' /proc/interrupts                        # ③
cat /sys/class/net/eth0/device/numa_node                   # ④
dpdk-l3fwd -l 2-5 -a 0000:18:00.0 --socket-mem=1024,0 \
  -- --config='(0,0,2),(0,1,3)'                            # ⑤

① Match queue count before you compare packet paths. Four AF_XDP queues versus one kernel queue is not an API result.

② RSS policy decides which queues even see the flow mix.

/proc/interrupts tells you whether one core still owns the hot queue or whether your placement assumptions already failed.

④ The NIC's NUMA node must match the CPU and memory plan you think you are testing.

⑤ In DPDK, dedicated cores, socket-local memory, and queue-to-core ownership are part of the launch contract, not late tuning.

Suricata's AF_XDP guide warns that wrong-NUMA hugepage placement can prevent startup in some modes, and Open vSwitch recommends keeping AF_XDP memory, PMD cores, and NICs on the same NUMA node. DPDK 26.07 makes the same point for mempools, rings, and queues. If a queue runs on socket 0 and its packet memory lives on socket 1, neither AF_XDP nor DPDK got a fair test.

If you need kernel TCP/TLS, packet ownership may be the wrong first move

Packet ownership is only a win when your application can also own the semantics that come with it. AF_XDP and DPDK expose packets, not kernel TCP streams. For a conventional HTTP or TLS reverse proxy, kernel sockets often remain the right baseline because Linux already owns congestion control, retransmission behavior, connection setup, and a lot of the observability surface. Episode two showed the same point from another angle: once a path needs a new encrypted representation, ownership changes for a reason.

That does not make AF_XDP or DPDK bad fits. It narrows their best cases. AF_XDP fits better when you already want queue-bound packet ownership for custom UDP, QUIC, telemetry, L4 filtering, or selective redirection. DPDK fits better when you intentionally want a user-space dataplane or even a user-space stack. F-Stack is a concrete reminder that ordinary TCP services on DPDK often pull in a user-space network stack too.

PathWhat you keepWhat you must add
Kernel sockets + XDPss, TCP stats, conntrack, mature socket behaviorXDP program counters and map visibility
AF_XDPethtool, bpftool, perf, XDP stats, driver countersapp packet counters, ring backpressure, mode logging
DPDK AF_XDP PMDNIC counters, PMD xstats, DPDK telemetry, AF_XDP mode truth underneathmost transport metrics, plus AF_XDP mode logging and NAPI policy awareness
DPDK native PMDNIC counters, PMD xstats, DPDK telemetrymost transport metrics, recovery logic, and app-visible queue health

AF_XDP exposes XDP_STATISTICS and actual mode through socket options. DPDK 26.07 has a telemetry library and PMD statistics interfaces, but your app has to publish and interpret them. Security shifts too. AF_XDP keeps Linux in charge of the netdev and validates redirection against the bound device and queue. DPDK can still be isolated safely with VFIO, IOMMU protection, and SR-IOV-aware deployment patterns, but one process now owns much more device and memory policy. That is not automatically unsafe. It is a larger blast radius.

Pick the narrowest boundary move that solves the measured problem

You do not need to move all the way to DPDK just because XDP alone stopped one bottleneck. Sometimes you should stay in XDP. Cloudflare's L4Drop is the clean example: they kept mitigation logic in XDP because early selective handling solved the real problem. Katran shows the same design instinct in an L4 load-balancing context: move the boundary only as far as the workload earns.

That is why I prefer a constraint-first table over a winner column. And this is where I want the taxonomy to stay honest: DPDK 26.07's AF_XDP PMD is a DPDK sub-mode, not a peer worldview, because the application model is still DPDK while the queue contract underneath is still AF_XDP.

OptionBoundary movedKeep kernel TCP/TLS semanticsZero-copy truth to verifyPolling realityRollback costBest fit
Kernel sockets + XDP-onlyLeastYesn/akernel NAPILowestConventional TCP/TLS plus early filtering
AF_XDP copy modeQueue handoff inside LinuxNocopied into UMEMmixed: kernel NAPI + user ringsMediumHybrid selective packet processing
AF_XDP zero-copy modeSame boundary, direct frame pathNoonly if XDP_OPTIONS_ZEROCOPY says somixed: kernel NAPI + user ringsMediumHigh-rate selective packet processing on proven drivers
DPDK native PMDUser-space device hot pathNoDMA -> mbufsexplicit user-space spinHighestDedicated network function virtualization (NFV) boxes, appliances, custom dataplanes
DPDK AF_XDP PMDDPDK app model over AF_XDP queuesNoinherits AF_XDP copy/zero-copy support underneathinherits AF_XDP busy-poll prerequisites plus DPDK schedulingHighDPDK applications that must share NICs or keep more Linux integration

Constraint first. Not ideology first. That is the whole point.

Production failures worth reading before you deploy

Most public writeups here are AF_XDP stories, because that is where the ecosystem has published the most candid production debugging. The lesson carries farther than AF_XDP: boundary moves fail at interfaces, not at slogans.

  • Namespace-aware AF_XDP setup can fail in real topologies. Cloudflare documented a production debugging story where AF_XDP, network namespaces, and shared state needed a cookie-based fix rather than a cleaner theory. Source
  • Packet corruption can come from kernel edge cases, not just user-space mistakes. Cloudflare traced occasional corrupt or truncated AF_XDP packets to a veth race and wrote up the full investigation. Source
  • Experimental integrations may skip cases you assume are covered. Open vSwitch still labels its AF_XDP netdev support experimental and notes skipped CVLAN tests because of kernel issues. Source
  • The selected AF_XDP interface may stop being a normal host interface. Suricata's AF_XDP guide warns that an interface used that way cannot also serve regular networking traffic in the ordinary way. Source

Those are not hypothetical footnotes. They are the cost of moving the boundary.

Benchmark the boundary, not the brand

A fair benchmark compares equal semantics. For raw-packet RX, drop, inspect, and L2/L4 forwarding work, compare AF_XDP copy mode, AF_XDP zero-copy mode, DPDK AF_XDP PMD, and DPDK native PMD separately AF_XDP documents the mode split and DPDK 26.07 documents the AF_XDP PMD as its own driver path. For conventional HTTP or TLS services, add kernel sockets plus XDP prefiltering as the baseline because that keeps transport ownership constant. If you compare AF_XDP or DPDK to a full TCP/TLS service without equalizing stack ownership, the chart mostly measures which system is doing more work.

Hold these constant: NIC and firmware, queue count, RSS policy, MTU, XDP mode, PMD choice, CPU pinning, IOMMU state, hugepage placement, generator, packet-size mix, offered load, and whether busy-poll prerequisites like napi_defer_hard_irqs and gro_flush_timeout are enabled for AF_XDP-based paths. Publish these: throughput, p50/p99/p99.9 latency, drops, CPU per core, cycles per packet, the actual AF_XDP mode used, and whether the DPDK AF_XDP PMD ran with busy polling enabled. DPDK 26.07 says burst size alone shifts the latency/throughput point. The AF_XDP latency study that tested about 400 configurations and reported a best-case RTT around 6.5 µs also reported 5–10 µs of tracing overhead in its setup. Even the profiler can move the result.

Use Linux counters on the AF_XDP side: ethtool -S, bpftool, /proc/interrupts, perf, XDP_STATISTICS, and XDP_OPTIONS_ZEROCOPY. Use DPDK telemetry, PMD xstats, and app counters on the DPDK side. If a chart does not disclose mode, queue layout, NUMA placement, and busy-poll policy, treat the number as marketing, not evidence.

Key takeaways

  • AF_XDP and DPDK do not “remove the kernel” in the same way; they move the ownership boundary to different places.
  • AF_XDP's core contract is queue-bound frame delivery through UMEM and four rings, with Linux still owning much of the platform.
  • DPDK's AF_XDP PMD is still DPDK, but it inherits AF_XDP's copy-versus-zero-copy truth and AF_XDP's busy-poll prerequisites underneath.
  • Driver support, NUMA placement, polling policy, observability, and rollback cost usually decide earlier than a headline Mpps chart.
  1. List which flows still need kernel TCP/TLS semantics.
  2. Print the actual AF_XDP mode, driver, queue, and NUMA node at startup.
  3. Co-locate queue, core, and memory before comparing APIs.
  4. Treat observability and rollback as part of the design, not cleanup work.

What's next: In episode five, the same ownership test moves to storage: page cache, O_DIRECT, io_uring, and NVMe queues.


Sources and References

Linux networking contracts

DPDK architecture and operations

Production reports and workload references

  • AF_XDP namespace failure analysis: Cloudflare debugging story - Cloudflare
  • AF_XDP packet corruption investigation: Cloudflare debugging story - Cloudflare
  • Selective XDP mitigation in production: L4Drop - Cloudflare
  • AF_XDP operational modes and caveats: Open vSwitch AF_XDP documentation
  • AF_XDP NUMA and interface-usage caveats: Suricata AF_XDP documentation - Suricata Docs
  • XDP-first dataplane reference: Katran repository - GitHub
  • User-space stack comparison class for DPDK deployments: F-Stack repository - GitHub

Benchmarking and analysis

  • AF_XDP latency study and instrumentation caveat: arXiv paper - arXiv

Share this post

HNPost to Hacker News
Subscribe:RSS feed

Keep reading