Article View

Scroll down to read the full article.

UDP Datagrams Vanishing in the Void: Node.js, Kubernetes, and Mellanox Mayhem

calendar_month July 25, 2026 |
Quick Summary: Debugging mysterious Node.js UDP packet loss in Kubernetes on Linux 5.10 with Mellanox NICs. Discover the root cause and fix for silent datagram d...

UDP Datagrams Vanishing in the Void: Node.js, Kubernetes, and Mellanox Mayhem

Alright, listen up, because I’m still seething over the weeks we lost to this one. You’ve got a Node.js UDP service in Kubernetes. It’s critical. It’s low-latency. It’s supposed to be bulletproof. Then, suddenly, under load, your upstream sends 10,000 datagrams, and your service only sees 7,000. Where did the other 3,000 go? Poof. Vanished. No errors. No warnings. Just… gone.

You’re tearing your hair out. You checked your Node.js application logic. You added more logging. You looked at CPU, memory. Everything seems fine. But the packets just aren't showing up. This isn't your typical ECONNRESET from a stealthy router timeout; this is pure, unadulterated network black magic.

The Symptoms: Silence, Then Panic

  • Intermittent, significant UDP packet loss (20-40%) under moderate to high load.
  • No visible errors in Node.js – the 'message' event simply isn't firing for missing packets.
  • netstat -su shows zero UDP buffer overruns on the host or in the container. Your socket receive queue isn't full.
  • tcpdump on the host network interface (e.g., eno1, eth0) shows the packets arriving.
  • But tcpdump inside the container's namespace on its eth0 (the veth pair) shows fewer packets. Or worse, it shows them but Node.js still doesn't get them.
  • Problem predominantly occurs on specific hardware/kernel combinations.

A digital void with disappearing data packets
Visual representation

The Frustrating Trail of Failed Leads

You’ve tried everything, right? Increased net.core.rmem_max and net.core.wmem_max. Bumped Node.js dgram socket buffer sizes. Checked ulimit -n. Swapped container base images. Nothing. You even started questioning Node.js itself, maybe blaming the event loop for being too slow. But you've seen Node.js handle high-throughput UDP traffic like a champ before. This felt different.

This particular beast reared its ugly head in a very specific environment. If you're running any of the following, pay close attention:

Component Version/Type Notes
Operating System Ubuntu 20.04 LTS (Kernel 5.4.x - 5.10.x), RHEL 8.x (Kernel 4.18.x - 5.10.x) Specifically kernels in the 5.x range with certain backports.
Node.js 14.x, 16.x, 18.x Any recent LTS version, the issue isn't Node.js specific code.
Network Card Mellanox ConnectX-5, ConnectX-6 Using mlx5_core driver (versions 5.x.x+).
Container Runtime Docker 20.10+, Containerd 1.6+ Kubernetes 1.20+ environments.

The Root Cause: NAPI Polling, Busy Polling, and the Mellanox Dance

After days of strace, perf, and endless printk debugging on custom kernel modules, we finally uncovered the horrifying truth. This isn't a Node.js bug, and it's not even a simple UDP buffer overflow. It's a subtle, insidious interaction between kernel network stack optimizations, specific NIC driver behavior, and the way container networking manages packet flow.

Modern Linux kernels use NAPI (New API) for network processing, which combines interrupts with polling to reduce CPU overhead. Many Mellanox NICs, with their mlx5_core driver, are highly optimized for this, often defaulting to aggressive polling for high-throughput TCP scenarios. However, for bursty UDP traffic, especially when combined with a relatively low net.core.busy_poll value or when the Node.js application process is occasionally CPU-starved (perhaps from other processes on the node or even synchronous I/O in Node.js itself), things go sideways.

Here’s the breakdown:

  1. Incoming UDP packets hit the Mellanox NIC.
  2. The mlx5_core driver, influenced by kernel parameters and its own internal heuristics, decides when to poll for new packets.
  3. These packets then need to traverse the container's veth pair, often involving NAT via iptables, before reaching the container's network namespace and ultimately the Node.js UDP socket.
  4. Crucially, the kernel's net.core.busy_poll and net.core.busy_read parameters determine how long the kernel will busy-wait for new packets before yielding. In environments with high-performance NICs, these defaults can be insufficient for bursty UDP that bypasses traditional TCP flow control.
  5. If the Node.js event loop is momentarily blocked (even for milliseconds, which can happen with garbage collection, or expensive synchronous operations that can be insidious to track down), or if the kernel isn't polling the veth queue aggressively enough for the incoming UDP stream, packets are silently dropped before they ever make it into the UDP socket's receive buffer. They effectively get lost in the software queues of the network stack, usually at the veth or bridge layer, before being handed to the user-space socket. It's like a bouncer at a club letting in a steady stream of VIPs but not bothering to check the general queue for a sudden rush, leading to people just giving up and leaving. For more on similar network nuances, check out The Ghost in the Wire: Node.js TLSv1.3 Hell on Linux 5.10 with i40e NICs, which touches on driver/kernel interactions.

An abstract network diagram with a bottleneck at a server's entrance
Visual representation

The Solution: Poke the Kernel, Prod the NIC

The fix, as often happens with these deep kernel issues, is ridiculously simple once you know it. You need to adjust the kernel's NAPI busy polling parameters to be more aggressive, giving the network stack more time to ingest bursty UDP packets, especially from the Mellanox driver's queue into the veth interface, before silent drops occur. This ensures that even if Node.js is briefly delayed, the kernel has a better chance of pushing packets into the socket buffer.

Apply these sysctl settings on your Kubernetes worker nodes. This is a host-level change, not a container-level one.


# Increase the busy_poll time to 500 microseconds
sudo sysctl -w net.core.busy_poll=500

# Increase busy_read to match busy_poll, ensuring longer busy-waiting on read calls
sudo sysctl -w net.core.busy_read=500

# For persistent changes, add to /etc/sysctl.conf
# net.core.busy_poll = 500
# net.core.busy_read = 500

A value of 500 microseconds (0.5ms) is often a good starting point. Test with higher values if loss persists, but be mindful of increased CPU utilization on idle network interfaces. This provides a crucial buffer for your Node.js application to process the incoming datagrams even if its event loop isn't always immediately available, preventing drops in the intermediate kernel queues. It's a similar principle to optimizing for sub-microsecond supremacy in algorithmic trading, where every CPU cycle and polling interval matters.

Final Words: Don't Blame Node.js Immediately

When you see mysterious packet loss, especially UDP, and your application code seems fine, always look deeper into the network stack and the underlying OS. It’s rarely Node.js itself causing these issues when you're talking about fundamental network transport. More often, it's a kernel configuration, a NIC driver quirk, or an unexpected interaction within a complex environment like Kubernetes. Save yourself the headache; check these obscure kernel tunables first.

Discussion

Comments

Read Next