Ch. 20 · Networking

UDP Delivery and Application Responsibilities

What UDP gives up compared with TCP, shown with real Node experiments, and the sequencing, pacing and retry work your application takes on.

~6 min readbeginnerupdated Oct 11, 2026

Calling UDP “the unreliable protocol” is accurate and not very useful. A better description: UDP is a thin wrapper around IP that delivers individual datagrams to a port, and leaves every other decision to you. When an interviewer asks “when would you use UDP over TCP?”, they want to hear that you understand what you’re giving up, what you have to build in its place, and why some applications are better off making those choices themselves.

The experiments below ran with Node’s dgram module on macOS, all on localhost.

What a datagram carries

A UDP header is eight bytes: source port, destination port, length and checksum. That’s the entire protocol. There’s no handshake, no connection state on either side, no acknowledgements, no sequence numbers and no flow control. The sender hands a datagram to the operating system, and it either arrives once, arrives twice, arrives out of order, or doesn’t arrive. Nothing tells the sender which.

Compare that with TCP, which spends a round trip setting up, numbers every byte, retransmits losses, slows down when the receiver or the network can’t keep up, and hides all of it behind a stream. UDP does none of that work, which is both its cost and its appeal.

Boundaries are kept, delivery isn’t promised

One difference works in UDP’s favour: each send is one datagram, and each datagram arrives as one message or not at all. Three sends from a client produced three separate events on the server:

got 3 bytes "one" from 127.0.0.1:64558
got 3 bytes "two" from 127.0.0.1:64558
got 5 bytes "three" from 127.0.0.1:64558
Text

You never need a length prefix or a delimiter to split UDP messages, which you always need over a TCP stream.

Now the less friendly behaviour. Sending to port 9199, where nothing was listening:

client.send('hello?', 9199, '127.0.0.1', (err) => console.log('send to 9199:', err ? err.code : 'no error'));
JavaScript
send to 9199: no error
Text

The callback reports success because, from the sender’s side, the datagram was handed off. The receiving host did reply with an ICMP “port unreachable” message, but an unconnected UDP socket ignores it. If you connect() the UDP socket to a fixed destination first, the operating system can match that ICMP reply to your socket, and Node surfaces it as an error event shortly after the send:

second send on connected socket: no error
connected socket error: ECONNREFUSED
Text

Even then the error arrives asynchronously, after the send callback said everything was fine, and only if the ICMP message makes it back. Plenty of firewalls drop it.

Size is the other surprise. On this Mac, a 20,000-byte send failed outright:

20,000-byte send: EMSGSIZE send EMSGSIZE 127.0.0.1:9100
Text

macOS caps datagrams at 9,216 bytes by default (net.inet.udp.maxdgram). IPv4’s theoretical ceiling is 65,507 bytes of payload, but the number that matters is the path MTU. A datagram bigger than roughly 1,400 bytes gets split into IP fragments, and losing any one fragment loses the whole datagram. That’s why QUIC keeps its packets to around 1,200 bytes and DNS resolvers limit how big a UDP answer can be before they switch to TCP.

Losing packets without any network

You don’t need a bad Wi-Fi connection to see UDP lose data. This test sent 5,000 datagrams of 1,000 bytes, each carrying a sequence number, to a receiver that was busy (a blocked event loop) for the first second:

const start = Date.now();
while (Date.now() - start < 1000) {} // receiver is busy for one second
JavaScript
sender: sent 5000, 0 send errors
receiver: got 762 of 5000, last sequence number 761
Text

The kernel’s receive buffer for the socket (about 786 KB here, net.inet.udp.recvspace) filled with the first 762 datagrams, and the remaining 4,238 were dropped. The sender saw zero errors. It had no way to know.

TCP in the same situation would have advertised a shrinking receive window and the sender would have slowed down. Nothing was lost, only delayed. With UDP, a slow consumer is a lossy consumer, and that includes a garbage collection pause or a busy thread in a perfectly healthy process.

What your application takes on

Choosing UDP doesn’t remove the problems TCP solves. It makes them yours, and lets you solve only the ones you care about:

  • Sequence numbers, so the receiver can spot loss, reordering and duplicates. The test above only knew what was missing because each datagram carried one.
  • A retransmission policy per kind of data. Some messages must arrive, some are worthless if late. TCP treats all bytes alike; you don’t have to.
  • Congestion control and pacing. A sender that blasts datagrams at full speed fills router queues and hurts every other flow on the link. RFC 8085 sets out what a well-behaved UDP application should do, and it’s not optional on a shared network.
  • Keepalives through NAT. There’s no connection for a router to track, so NAT devices forget idle UDP mappings after a timeout, often somewhere between 30 seconds and a few minutes. Long-lived UDP sessions send small periodic packets to stay reachable.
  • Spoofing protection. UDP has no handshake, so the source address isn’t verified. Servers that send large replies to small requests (old DNS and NTP setups) have been abused for amplification attacks. Don’t send a big response to an unverified address.

When UDP is the right call

A common misconception is that “UDP is faster”. Per byte, it isn’t meaningfully faster. What it avoids is the handshake and head-of-line blocking, where one lost TCP segment stalls everything behind it until it’s retransmitted.

That matters most where late data is useless. Take a game sending player positions 60 times a second. If one update is lost, the next arrives about 17 ms later and makes it obsolete. Waiting for a retransmission of the old position, as TCP would, delays the newer ones behind it. Voice and video calls work the same way: a dropped audio packet is a tiny glitch, a stalled one is a noticeable freeze.

DNS uses UDP because most queries are a single small question and answer, and retrying a lost query is cheaper than a handshake. StatsD metrics use it because losing an occasional counter increment doesn’t matter and the application should never block on telemetry.

Then there’s QUIC, the transport under HTTP/3. It runs over UDP and rebuilds reliability, ordering per stream, congestion control and encryption in user space. It picks UDP because operating systems and middleboxes already pass UDP through, and because a user-space protocol can avoid TCP’s cross-stream head-of-line blocking and ship improvements without waiting for kernel upgrades.

Interview practice

You’re designing the networking for a multiplayer game. Clients send their position 30 times a second, players can chat, and they can buy items in the game. What would you send over UDP, what needs reliability, and how would you handle each?

Answer

Positions go over UDP without retransmission. Each update carries a sequence number and a timestamp, and the receiver discards anything older than the newest it has applied. A lost update is replaced 33 ms later, so retransmitting it would only add stale data. The server should still pace what it sends and cut the update rate or detail for a client whose loss climbs, rather than flooding a congested link.

Chat needs every message, in order, but tolerates a little delay. I’d send it on a reliable channel: either a TCP or WebSocket connection, or a reliable, ordered stream inside the same UDP protocol (which is what QUIC or a game networking library provides), with message IDs and acknowledgements.

Purchases must happen exactly once and are worth real money, so they shouldn’t depend on game-loop packets at all. I’d send them as an HTTPS request with an idempotency key, so a retry after a timeout can’t buy the item twice, and let the server confirm the result before the client shows it. The general point is that UDP lets you pick a delivery guarantee per type of data, and a good design uses that instead of forcing everything through one guarantee.

More in Networking

read ✓Networking · hard

HTTP/3 and QUIC Transport

Why QUIC moves HTTP onto UDP, how browsers discover HTTP/3 through Alt-Svc and HTTPS records, and where its gains over HTTP/2 disappear.

~6 min readread →
read ✓Networking · mid

HTTP Connection Reuse and Latency

Measure what keep-alive saves, size a client connection pool with Little's law, and retry safely when a pooled socket was closed under you.

~6 min readread →
read ✓Networking · easy

TCP Byte Streams and Message Framing

Why three socket writes can arrive as one read and one write as seventeen, and how to frame messages with delimiters or length prefixes.

~6 min readread →
esc