Networking
UDP — User Datagram Protocol
Fire-and-forget packets in 8 bytes of header
UDP is a connectionless transport protocol that sends best-effort datagrams with an 8-byte header and no handshake. It powers DNS, video, gaming, and QUIC — anywhere latency matters more than reliability.
- Header size8 bytes
- HandshakeNone
- ReliabilityBest-effort
- OrderingNone
- Max payload65,507 bytes
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
How UDP works
UDP is the thinnest possible wrapper around an IP packet. The protocol adds four 16-bit fields — source port, destination port, length, checksum — then hands the kernel your bytes and walks away. There is no connection state, no acknowledgement, no retransmission, no flow window, no congestion control. If the packet is lost, corrupted, duplicated, or reordered, that's the application's problem.
That sounds like a downside. It's the entire point. TCP's reliability machinery costs a 3-way handshake (1 RTT before any data flows), per-segment ACKs (every byte tracked), and head-of-line blocking (one lost segment freezes the receiver's read). For a DNS query that fits in 80 bytes, paying a full RTT just to open a connection is absurd. For a voice packet, retransmitting a 20ms sample 200ms later is worse than dropping it.
The UDP header looks like this on the wire:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port (16) | Destination Port (16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (16) | Checksum (16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
That's it. Compare to TCP's 20-byte minimum (often 32+ with options) — UDP carries 12 fewer bytes of overhead per packet, which on a 1500-byte MTU is nearly 1% pure efficiency.
When to use UDP
- Loss-tolerant streams. Voice, video, live game state — a stale packet is worse than no packet.
- Tiny request/response. DNS, NTP, SNMP — the request is a single packet; a handshake would dominate the cost.
- Multicast and broadcast. TCP cannot multicast; UDP is the only option for one-to-many distribution at the transport layer.
- You want to write your own reliability. QUIC, RTP, WebRTC's data channel, and most modern game netcode run on UDP precisely so they can implement smarter loss recovery than TCP's ACK-and-retransmit.
UDP vs other transports
| UDP | TCP | SCTP | QUIC | |
|---|---|---|---|---|
| Header size | 8 B | 20-60 B | 12 B + chunks | Variable, ~1-15 B |
| Connection setup | 0 RTT | 1 RTT (3-way) | 2 RTT (4-way) | 1 RTT (0 with resumption) |
| Reliability | None | In-order, retransmit | Per-stream, configurable | Per-stream, retransmit |
| Ordering | None | Strict | Per-stream | Per-stream |
| Congestion control | None | Yes (Reno, Cubic, BBR) | Yes | Yes (pluggable) |
| Multistreaming | N/A | One stream per socket | Multiple per association | Multiple per connection |
| NAT/firewall friendliness | Mediocre | Excellent | Poor | Excellent |
| Built on | IP | IP | IP | UDP |
QUIC's choice to ride on UDP rather than IP directly was pragmatic: middleboxes and home routers know how to forward UDP. New IP-level protocols are blocked or mangled by routers that haven't been updated since 2010.
JavaScript implementation — chat server with dgram
// Node.js UDP chat server (broadcasts each message to all known clients)
import dgram from 'node:dgram';
const server = dgram.createSocket('udp4');
const clients = new Map(); // key: "ip:port" -> { address, port }
server.on('message', (msg, rinfo) => {
const key = `${rinfo.address}:${rinfo.port}`;
clients.set(key, rinfo);
const text = msg.toString().trim();
console.log(`[${key}] ${text}`);
// Fan out to everyone else
for (const [k, peer] of clients) {
if (k === key) continue;
server.send(`<${key}> ${text}`, peer.port, peer.address);
}
});
server.on('listening', () => {
const a = server.address();
console.log(`UDP chat listening on ${a.address}:${a.port}`);
});
server.bind(41234);
Notice what's missing: no accept(), no per-client socket, no connection state tracking beyond what we built ourselves. Each message event is one datagram from one peer; rinfo tells us where it came from. A client can disappear and reappear without the server ever noticing.
Python implementation — asyncio.DatagramProtocol
import asyncio
class ChatServer(asyncio.DatagramProtocol):
def __init__(self):
self.clients = {} # (ip, port) -> transport addr
self.transport = None
def connection_made(self, transport):
self.transport = transport
def datagram_received(self, data, addr):
self.clients[addr] = addr
text = data.decode('utf-8', errors='replace').strip()
print(f"[{addr[0]}:{addr[1]}] {text}")
for peer in self.clients:
if peer == addr:
continue
self.transport.sendto(
f"<{addr[0]}:{addr[1]}> {text}".encode(),
peer
)
async def main():
loop = asyncio.get_running_loop()
await loop.create_datagram_endpoint(
ChatServer, local_addr=('0.0.0.0', 41234)
)
print("UDP chat listening on 0.0.0.0:41234")
await asyncio.Event().wait() # run forever
asyncio.run(main())
A 1,000-byte chat broadcast to 100 clients costs roughly 100 datagrams * (8 B UDP header + 20 B IPv4 header + payload) = 102.8 KB of egress. The same logic over TCP would need 100 connections, each tracking sequence numbers, ACKs, and a sliding window — easily 10× the kernel memory.
Concrete costs
- Header overhead: 8 bytes per datagram. TCP starts at 20 bytes and is typically 32 with timestamps and SACK. On a 200 B DNS response, UDP overhead is 4%; TCP is 14%.
- Setup cost: Zero RTT. The first packet carries application data. TCP+TLS 1.3 needs 2 RTTs; TCP+TLS 1.2 needs 3. Over a 50 ms link, that's 0 vs 100-150 ms before any data flows.
- Per-packet cost in the kernel: A modern Linux UDP socket can push 10-15 Mpps with
sendmmsg; TCP tops out around 1-3 Mpps because of stream state and Nagle bookkeeping. - MTU ceiling: Exceed path MTU (typically 1500 bytes Ethernet, 1280 IPv6) and the IP layer fragments. Fragments are routinely dropped by middleboxes — most production UDP code caps payloads at 1200-1400 bytes to be safe.
Variants and extensions
- UDP-Lite (RFC 3828) — replaces the length field with a checksum-coverage field, so codecs (audio, video) can opt to ignore bit errors in their payload while still validating the header. Useful on lossy radio links where a few flipped pixels are better than dropping the whole frame.
- GUDP (Generic UDP Encapsulation, RFC 8086) — wraps arbitrary IP packets inside UDP for tunneling through middleboxes that mangle anything that isn't UDP/TCP. Used by L2TP, MPLS-over-UDP, and Geneve.
- UDP-encapsulated IPsec (RFC 3948) — when an IPsec ESP packet would be eaten by a NAT, the kernel wraps it in UDP/4500.
- DTLS — TLS adapted for UDP. Adds replay protection and explicit sequence numbers since UDP doesn't have them.
- QUIC — a full reliable, multiplexed, encrypted transport built on top of UDP. Strictly speaking, QUIC is the reason UDP rules the modern internet.
Common bugs and edge cases
- Silent drops at the MTU boundary. A 1500-byte send works in dev; the same code drops in production because the path MTU is 1492 (PPPoE) or 1280 (IPv6 over GRE). Use
IP_MTU_DISCOVER+IP_MTUor just clamp payloads at 1200 B. - Receive buffer overflow. Default
SO_RCVBUFis 208 KB on Linux. A burst of 200 small packets fills it;recvfromdrops the rest with no error visible to the sender. Bump to 4-8 MB for high-throughput receivers. - Spoofing and amplification. Source IPs are not validated. DNS amplification attacks send a 60 B query with a spoofed source to an open resolver, which replies with 4 KB to the victim — a 60× amplification.
- NAT timeout. NATs evict UDP mappings after 30 s of idle (vs 2 hours for TCP). Long-lived connections need an application-layer keepalive every 15-25 s.
- Checksum offload bugs. Some NICs compute the UDP checksum in hardware. If offload is broken, the packet ships with checksum 0 — fine on IPv4, dropped on IPv6.
ethtool -K eth0 tx-checksum-ipv6 offdiagnoses it. - Connect on a UDP socket. Calling
connect()on a UDP socket doesn't open anything — it just filters incoming packets to one peer and lets you usesendinstead ofsendto. Useful, but trips beginners.
Frequently asked questions
Why use UDP instead of TCP?
When latency, retransmits, or head-of-line blocking would hurt more than packet loss does. Voice, video, gaming, DNS, and time-sync (NTP) all prefer a missed sample over a 200ms stall waiting for a retransmit.
Is UDP unreliable?
UDP itself offers no delivery guarantees, no ordering, no flow control, and no congestion control. Applications layer their own reliability on top — that's exactly what QUIC, RTP, and most game netcode do.
What is the maximum UDP payload size?
Theoretically 65,507 bytes (65,535 minus IP and UDP headers), but anything larger than the path MTU (typically 1500 bytes on Ethernet, 1280 on IPv6) gets fragmented. Fragmentation is so unreliable in the wild that most applications cap at ~1200-1400 bytes.
Why does DNS use UDP?
A typical DNS query and response fit in one packet each. UDP avoids the three-way handshake — DNS would otherwise spend 1 RTT just opening a TCP connection before sending the query. DNS does fall back to TCP for responses larger than 512 bytes (or 4096 with EDNS0).
Does UDP have a checksum?
Yes, UDP carries a 16-bit checksum over the header, payload, and a pseudo-header from IP. It's optional in IPv4 (a value of 0 means no check) and mandatory in IPv6. UDP-Lite extends this so only part of the payload is covered, useful for codecs that can tolerate bit errors.
Can UDP packets arrive out of order?
Yes. UDP makes no ordering promise — datagrams may arrive reordered, duplicated, or not at all. If your application cares about order, include a sequence number in the payload.