UDP Sockets

You are on a video call. Someone's face freezes for a quarter-second, a syllable turns to static, and then — without anyone asking — the picture snaps back to the present moment. Nobody rewound. The lost frame was simply abandoned: by the time it could have been re-sent, it was already stale, and a fresh frame had taken its place. That "throw it away and move on" behaviour is the whole personality of the protocol underneath: UDP, the User Datagram Protocol, and the sockets you use to speak it.

You have already met the socket API through its most common guise — the reliable, ordered stream socket backed by TCP. This page is about its wilder sibling: the datagram socket (SOCK_DGRAM), backed by UDP. A datagram socket trades away TCP's guarantees — no connection, no handshake, no retransmission, no ordering — for two things you cannot get any other way: speed, and message boundaries that are actually preserved. The programming model is smaller, but the responsibilities it hands back to you are larger. Both halves of that bargain are the subject of this page.

A different shape of API: no connection to set up

Recall the seven-call TCP dance: socket → bind → listen → accept on the server, socket → connect on the client, then read/write, then close. UDP throws most of that away. There is no connection, so there is nothing to establish and nothing to accept. A UDP program calls socket(), optionally bind()s a port (a server must; a client usually needn't), and then simply sends and receives datagrams — each one carrying its destination (or source) address with it, packet by packet.

The two workhorse calls are sendto() and recvfrom():

Because there is no connection, one UDP socket can converse with thousands of peers at once through a single file descriptor — every datagram is self-addressed. There is no accept() spawning a socket per client, and often no per-client state in the kernel at all. This is exactly why DNS resolvers, which field a torrent of tiny independent queries from everywhere, live on UDP.

The two lifecycles, side by side

Put the TCP and UDP server loops next to each other and the missing machinery jumps out. UDP has no listen(), no accept(), no handshake, and no per-connection socket — just a bound port and a recvfrom/sendto loop.

Here is a UDP echo server and client as illustrative (non-runnable) code — real datagrams need a real network, which the sandbox does not have. Notice the address travelling with each call:

// --- UDP SERVER --- one socket serves everyone; no accept, no per-client state const sock = createDatagramSocket(); sock.bind(5353); // claim a port while (true) { const [data, from] = sock.recvfrom(); // ONE whole datagram + its sender sock.sendto(data, from); // echo it back to that exact address } // --- UDP CLIENT --- no connect(); name the destination on every send const c = createDatagramSocket(); c.sendto("ping", { host: "server.example", port: 5353 }); const [reply, _from] = c.recvfrom(); // one datagram back, or block forever if lost console.log(reply.toString()); // "ping"

The good news: message boundaries survive

The single most painful bug in TCP programming is that write("hello") then write("world") can arrive as helloworld, or hel then loworld — TCP is a faucet of bytes and does not remember where one write ended. UDP is the exact opposite. One sendto becomes one datagram becomes one recvfrom. The message you sent is the message that comes out, whole, or it does not come out at all — never smeared into its neighbours.

The demo below models both protocols over the same three logical messages, so you can watch the boundaries dissolve under TCP and hold under UDP.

// Three application-level messages. const messages = ["LOGIN alice", "MSG hi there", "LOGOUT"]; // --- TCP model: writes are concatenated into ONE stream, chunked arbitrarily --- const stream = messages.join(""); const tcpArrivals = ["LOGIN aliceMSG h", "i ther", "eLOGOUT"]; // how bytes happen to land console.log("TCP (stream): a naive '1 read = 1 message' reader sees..."); for (const chunk of tcpArrivals) console.log(` recv -> "${chunk}"`); console.log(` ...boundaries GONE: ${messages.length} messages smeared into ${tcpArrivals.length} reads.\n`); // --- UDP model: each sendto is a self-contained datagram; each recvfrom returns one --- const datagrams = messages.map((m) => ({ payload: m })); // one datagram per message console.log("UDP (datagram): each recvfrom() returns exactly one whole message..."); for (const d of datagrams) console.log(` recvfrom -> "${d.payload}"`); console.log(" ...boundaries PRESERVED: no framing code needed. What you sent is what you get.");

Read that contrast carefully. On TCP you must add your own framing to recover messages. On UDP the framing is free — the datagram is the frame. If your data is naturally message-shaped (a DNS query, a sensor reading, a game state update), that alone can make UDP the more honest fit.

The bad news: everything else is now your job

UDP hands you message boundaries and then walks away. The network is free to lose your datagram, duplicate it, or reorder it relative to its siblings, and UDP will never tell you. If your application needs any of the guarantees TCP gave away, you must rebuild them yourself — on top of UDP.

This is why the modern web's fast transport, QUIC (the basis of HTTP/3), is built on UDP: it wanted to design its own reliability, ordering, and congestion control from scratch — without waiting for a decade of TCP to change in every operating-system kernel — so it started from the blank slate UDP provides.

Two traps snare almost everyone the first time. First, you own reliability. UDP will silently lose, reorder and duplicate datagrams and never say a word; a client that does a single recvfrom() and waits for a reply that was dropped will block forever. If your data must arrive, you must add sequence numbers, acknowledgements, timeouts and de-duplication — i.e. you must build the reliability TCP would have given you for free.

Second, size matters. A datagram is one packet's worth of payload. Send one larger than the path's MTU (~1500 bytes on typical Ethernet, often safely ~1200 for internet paths) and IP must fragment it into several packets — and if any one fragment is lost, the whole datagram is discarded, sharply raising your loss rate. Worse, some middleboxes just drop fragments outright. The rule of thumb: keep UDP payloads comfortably under the MTU, and if you need to send more, split it into MTU-sized, individually-recoverable messages yourself.

So when do you actually choose UDP?

The decision is really about what lateness costs you. TCP guarantees delivery, but the price of that guarantee is waiting: a lost segment stalls everything behind it until it is re-sent (head-of-line blocking). For some data, a late byte is worse than a missing one.

UDP's header is a mere 8 bytes: source port, destination port, length, and a checksum. That checksum lets the receiver detect a corrupted datagram (and silently drop it) — but there is no field for a sequence number, an acknowledgement, or a window, because UDP has no notion of ordering, delivery or flow. The design philosophy, straight from its 1980 spec (RFC 768), is "a thin shim over IP that adds just ports and error-detection, and gets out of the way." Everything richer is left to the application — which is precisely the freedom that lets QUIC, DNS and your favourite game all live on the same eight-byte header.