Links that I found useful this week:
TL;DR
UDP is a TCP alternative that has great performance at the cost of reliability. Here are some key points to consider:
Performance vs Reliability: UDP is more performant than TCP (no handshake, smaller packets) but offers no delivery guarantees, packet ordering, or error recovery
Node.js Performance Limitations: Node.js UDP implementation involves 3-4 data copies compared to 1 copy in C/C++, plus garbage collection can cause latency spikes under high throughput
dgram Module: Use
dgram.createSocketandbind(notlisten) to create UDP servers; optionallyconnectto associate with a destination for easier sendingUnder the Hood: Your code → dgram module → UDPWrap (C++) → libuv → OS system calls → kernel UDP stack → network interface
Understanding the pros and cons of UDP
Both TCP and UDP are transport-layer protocols.
When we establish a connection using TCP, we have to wait for the three-way handshake to complete. This process ensures
that the connection is reliable and both client and server are ready to exchange data. You can learn more about the TCP handshake, sockets, and how it works in Node.js if interested.
UDP is nothing like that. If we're talking about raw UDP, then it does not care whether a connection is established or a client is ready to receive the data.
In fact, there is no "connection" at all, and the protocol is often called "connectionless". All it cares about is just firing pieces of data, also known as packets, to the final destination.
Thanks to the simplicity of UDP and smaller packet size, it tends to be faster than TCP-based alternatives.
The downsides are:
No packet delivery guarantees
No packet ordering assurance
No congestion control
No automatic retransmission
No duplication control
Simply put, with UDP, you send the data, but whether it arrives in the same order or arrives at all is not guaranteed.
That's the price of the performance and simplicity.
Node.js-specific UDP issues
We see that UDP servers are about efficiency and performance. But when we're talking about efficiency and performance, the question arises: "Can Node.js even deliver on that promise with its UDP server?"
Well, there are certain downsides of having UDP servers written in Node.js compared to languages like C or C++.
No zero-copy operations
A packet goes through the layers of abstractions before it gets to the application memory, where you can actually operate with it.
It's not always free to go through those layers. At times, data has to be copied over from one layer to another in order to proceed further down.
In the UDP server, we want to see the least number of such operations for better performance and lower latency.
When working with Node.js, data has to go through quite a few layers to get to the application, where we can finally operate with it.
You don't have to understand every part of the path. The important thing here is that we can end up doing 4 copies of the same data over the course of handling a single packet.
Now compare this path to the path in languages like C or C++.
It's a pretty standard workflow of a non-optimized UDP server, and you already get only a single copy, which is 3 to 4 times more efficient than what we have in Node.js.
Things can be pushed even further using DPDK (Data Plane Development Kit) that allows completely bypassing the kernel and directly accessing the network card's DMA, which is the true zero-copy operation.
Garbage collection is a potential performance hit on high throughput
When talking about high-throughput UDP servers, we want to be sure that even the smallest things are tuned properly to achieve the best performance. It includes optimal memory management, which Node.js can't flex about. Unpredictable garbage collection can cause latency spikes that are unacceptable.
Overall, if there is no need to get the absolute best out of a UDP server in terms of performance, then Node.js could still be an acceptable option.
Node.js dgram module
Node.js provides a dgram module, which is an abstraction that lets us work with UDP in Node.js. There is a single core abstraction of the module that we're interested in, it is the `dgram.Socket` class. Let's look at its basics.
Creating a socket
If you read Node.js documentation on the dgram module, you'll find that code snippets there use the dgram.createSocket method to create a socket.
Most of the time, when you have an abstraction to create a class, you'd assume that something else is going on behind the scenes. But it's not the case with dgram.createSocket. All it does is take 2 arguments and just pass them to the dgram.Socket constructor. That's it.
function createSocket(type, listener) {
return new Socket(type, listener);
}You can see the source code for yourself.
In terms of functionality, it doesn't matter if you call the factory function or use the constructor directly. However, I'd stick with the factory function to keep consistency with the documentation. That way, you're safe from future changes in the socket creation process, where some additional changes might be introduced in the factory function but not in the constructor.
Binding a socket
If you have any experience working with TCP-based servers in Node.js, then you probably used the server.listen method to bind a socket to a port and make it available for incoming connections.
In the case of UDP, there is no listen method, which kind of makes sense because there is no connection to establish; UDP is connectionless. You're just saying that "I want to receive data on that port." To do that, you need to call the bind method.
const dgram = require("node:dgram");
const socket = dgram.createSocket("udp4");
socket.bind(4321, "127.0.0.1");Sending data
Sending data is pretty straightforward. There is a send method that you can use to do exactly that. However, when using the send method, you need to specify the address and port of the destination. It might be inconvenient to do that every time. For that reason, there is a connect method that you can use to associate a socket with an address and port. After that, you can simply call send without specifying the address and port.
The same is stated in the official documentation:
const dgram = require("node:dgram");
const socket = dgram.createSocket("udp4");
socket.bind(4321, "127.0.0.1");
socket.connect(4321, "127.0.0.1");
socket.send("Hello World!");With that, you're all set to start sending/receiving data.
Dgram socket under the hood
When creating a new socket with new Socket, a lot is happening under the hood to make it possible to receive/send data from a Node.js application.
Let's get a closer look at the process.
Node.js internals, bindings, and libuv
First, the call of the new Socket creates a handle that associates the JavaScript Socket class instance with a low-level UDP socket.
It happens by calling the newHandle function. The newHandle uses the C++ binding UDPWrap to create a new UDP socket.
Well, it uses UDP constructor, but it's just remapped name of the UDPWrap class.
In the same UDPWrap source code, you can find references to uv_udp_t, which is the libuv handle that represents the UDP socket.
So far, we have the following:
OS and Kernel layers
The next layer is the OS. The uv_udp_t handle internally uses system calls to create and manage the actual UDP socket.
The most important system calls are:
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)- creates the UDP socketbind()- binds to a port/addresssendmsg()/sendto()- sends UDP datagramsrecvmsg()/recvfrom()- receives UDP datagrams
For async I/O, libuv integrates with platform-specific event notification systems:
Linux: epoll
macOS/BSD: kqueue
Windows: IOCP (I/O Completion Ports)
At the kernel level, the socket is represented by a file descriptor (just an integer in userspace) that maps to kernel structures managing the UDP protocol, IP layer, and network device drivers.
You can actually see this file descriptor from Node.js:
const socket = dgram.createSocket("udp4");
console.log(socket._handle.fd); // e.g., 20The full path looks like the following:
Wrap Up
UDP is a great choice for cases where you need low latency and less overhead; however, this performance comes at the cost of reliability. When it comes to Node.js implementation of UDP, it also adds performance overhead through multiple data copies and potential garbage collection pauses that languages like C/C++ don't have.
The dgram module makes UDP accessible and relatively straightforward to implement. Understanding the full stack from your JavaScript code through C++ bindings to libuv to the kernel
helps you make informed decisions about when UDP fits your architecture.
If you're comparing protocols, check out my deep dive into TCP and Node.js server internals to see how these two transport protocols stack up in practice.
What's your experience with UDP in Node.js? Have you used it in Node.js? Perhaps you run into performance bottlenecks that made you reconsider your protocol choice?






