0%
Networking for Backend Engineers
The Stack
Names and Trust
Application Protocols
Getting Traffic to a Server
Performance
UDP and When to Use It
UDP is often described as TCP without reliability, which is accurate and useless. The productive framing is that UDP is the thinnest possible interface to IP datagrams, and that choosing it means accepting a list of specific obligations that TCP would otherwise have discharged on your behalf. Knowing that list is the difference between a deliberate choice and an accident.
The header, in full
The entire UDP header is eight bytes: source port, destination port, length, checksum, each sixteen bits. That is the whole protocol. Compare it with TCP's twenty bytes before options, and notice what is absent rather than what is present: no sequence number, no acknowledgement number, no flags, no window, no options.
Every one of those absences is a feature TCP provides that you no longer have. No sequence number means no ordering and no duplicate detection. No acknowledgement means no reliability and no way to know a datagram arrived. No window means no flow control, so a fast sender can overwhelm a slow receiver with no feedback whatsoever. No congestion response means the network cannot slow you down by any mechanism other than dropping your traffic.
The length field is redundant with the IP header's length and exists for historical reasons. The checksum covers the header, the payload and a pseudo-header containing the source and destination addresses, which means it detects corruption of the addresses as well as the data. On IPv4 the checksum is technically optional and a zero value means unchecked; on IPv6 it is mandatory, because IPv6 removed the header checksum at the network layer and the transport is now the only thing verifying anything.
Datagram boundaries are preserved, which is the underrated advantage
TCP is a byte stream, so a sender's four 100-byte writes may arrive as one 400-byte read or as 400 one-byte reads, and every TCP application therefore needs framing: a length prefix, a delimiter, or a self-describing format. That framing code is a recurring source of bugs, particularly around partial reads and buffer management.
UDP preserves message boundaries exactly. One sendto becomes one datagram becomes one recvfrom, and the receiver either gets the whole message or nothing at all. For protocols whose unit of work is naturally a message, which includes DNS queries, metrics samples, log lines and position updates, this removes an entire category of code and the bugs that live in it.
The flip side is that a message larger than the datagram limit cannot be sent at all, which the next section covers, and that a partially received message is not a thing that can happen, so there is no resumption or streaming of a single message.
The port space, and who is allowed to listen
UDP ports are a separate 16-bit space from TCP ports, so a service can hold UDP 8125 while an unrelated process holds TCP 8125, and binding one tells you nothing about the other. This trips people up when reading ss output, where the -u and -t flags select different namespaces entirely, and when writing firewall rules, where a rule allowing TCP on a port does nothing for UDP traffic to the same number.
The ephemeral range, net.ipv4.ip_local_port_range, is shared in the sense that both protocols draw from it, but allocation is per protocol. A host exhausting TCP ephemeral ports can still open UDP sockets freely. Because UDP has no four-tuple reuse restriction and no TIME_WAIT, the port exhaustion failure mode that dominates TCP operations does not exist here, which removes one familiar worry and is worth stating explicitly because people arrive expecting it.
What does exist is a different binding question. Multiple sockets can bind the same UDP port if they use SO_REUSEPORT, and the kernel then hashes each arriving datagram to one of them by its four-tuple. This is the standard way to scale a UDP receiver across cores, and it has a sharp edge: the set of sockets is part of the hash, so adding or removing a socket reshuffles which socket receives which flow. For a stateless receiver this is harmless; for anything holding per-flow state, such as a QUIC server, it means a worker restart can send an existing flow's datagrams to a worker that has never seen it, which is why those servers either use SO_REUSEPORT with a connection identifier that lets any worker route the datagram onward, or avoid reshuffling by keeping the socket set stable.
There is no connection, but there is a connected socket
UDP has no handshake and no connection state in the kernel's transport layer, yet connect() works on a UDP socket and does something useful. It records the peer address in the socket, which has three effects worth knowing.
First, you can then use send and recv rather than sendto and recvfrom, which avoids re-specifying and re-resolving the destination on every call and is measurably faster, since the kernel skips the route lookup per datagram. Second, the socket will only receive datagrams from that peer, which filters out traffic from other sources. Third, and most importantly, the socket becomes eligible to receive ICMP errors for that destination, which is how a connected UDP socket can learn that the port is closed.
That third point is the usual answer to the question of how a UDP client ever discovers a failure. An unconnected socket sending to a closed port gets nothing: the ICMP port unreachable comes back, the kernel cannot attribute it to any particular socket, and it is dropped. A connected socket gets ECONNREFUSED from the next send or recv, which is the only cheap failure signal UDP offers.
What the kernel still does for you
It is worth being precise about the small amount UDP does provide, because it is more than nothing and the boundary matters.
Ports are demultiplexed, so two processes on one host can hold different UDP ports and receive only their own traffic. The checksum is computed and verified, so corrupted datagrams are dropped rather than delivered, which means you get silent loss rather than silent corruption. A receive buffer exists, so datagrams arriving faster than the application reads them are queued up to SO_RCVBUF rather than dropped immediately. Broadcast and multicast are available, which TCP cannot do at all. And SO_REUSEPORT lets multiple sockets bind the same port with the kernel hashing datagrams across them, which is how a multi-process UDP server scales.
What it does not do, and what no amount of socket options will add, is tell you whether anything arrived.
Multicast and broadcast, available only here
One capability has no TCP equivalent at all, and it is the reason some protocols could never have been built on anything else. A UDP datagram can be addressed to a broadcast address, reaching every host on the local segment, or to a multicast group address, reaching whichever hosts have joined that group.
Broadcast is the blunt version and is limited to the local segment, since routers do not forward it. It is how DHCP's first exchange works, since a host with no address cannot send a unicast packet to a server whose address it does not know. Multicast is the selective version: a receiver joins a group with IP_ADD_MEMBERSHIP, switches with IGMP snooping learn which ports have members and forward only there, and a single sent datagram reaches all of them without the sender knowing who they are.
The operational reality is that multicast works well inside a network you administer and is unavailable almost everywhere else, including across the internet and inside every major cloud provider's network. That asymmetry is worth holding onto, because it is the single most common reason software written for a corporate LAN fails immediately when it is moved.
The obligations you take on
RFC 8085 is the document that spells this out, and its guidance is more prescriptive than most people expect. An application using UDP is responsible for congestion control if it sends more than a trivial amount of data, for retransmission if it needs reliability, for ordering if it needs it, for duplicate suppression, for path MTU handling, and for not becoming an amplification vector.
That list is why the correct default is TCP. UDP is the right answer when one or more of those obligations is cheaper to discharge in your application than TCP's generic implementation of it, or when TCP's implementation actively conflicts with your requirements. Both of those situations are real and the rest of this lesson is about recognising them, but neither is the common case.
Reaching for UDP to avoid TCP's overhead is almost always a mistake when the overhead in question is the handshake, because the fix for that is connection reuse rather than a different transport. A warm, pooled TCP connection has no handshake cost, gets ordering, reliability and congestion control for free, and traverses middleboxes that may drop UDP entirely. Choose UDP when you need something TCP cannot give you, such as per-message independence, multicast, or your own loss recovery policy, not when you need TCP without its startup cost.