UDP: Connectionless Transport
Imagine a live video call where one lost frame should vanish, not stall the whole conversation. A reliable transport protocol would pause everything to retransmit that lost frame. It would arrive late and useless. This is the concrete problem that motivates the User Datagram Protocol (UDP): sometimes speed and simplicity matter more than guaranteed delivery. UDP strips the transport layer down to its bare essentials. It uses addressing via ports and a lightweight integrity check. Then it gets out of the application's way. UDP matters because huge swaths of the modern internet depend on it. DNS lookups, streaming media, online gaming, IoT telemetry, and even parts of HTTP/3 (via QUIC) run over UDP rather than TCP. Each of these applications tolerates occasional loss, or needs low latency more than perfect delivery. Others want to build custom reliability and flow-control logic instead of inheriting TCP's assumptions. Understanding UDP is therefore not about learning "the lesser protocol." It is about learning when connectionless, unacknowledged delivery is the right engineering choice. The big picture is a tradeoff axis. TCP trades latency and simplicity for reliability, ordering, and congestion control. UDP trades reliability for minimal overhead and full application control. UDP does not fix a stream, does not retransmit, and does not order segments. It also does not throttle its sending rate in response to congestion. It only does two things well: multiplex/demultiplex data to the right process using port numbers, and optionally detect (not correct) certain kinds of bit-level corruption using a checksum. This article gives a precise definition of UDP's segment format. It walks through the checksum algorithm step by step with a worked numeric trace. It explains exactly why UDP's minimalism is both a feature and a hazard, and situates UDP historically and architecturally. It builds directly on Transport-Layer Services and Multiplexing and sets up the machinery — sequence numbers, acknowledgments, timers — developed in Principles of Reliable Data Transfer.
- UDP is a connectionless, unreliable, message-oriented transport protocol with an 8-byte header.
- It provides only two services beyond IP: multiplexing/demultiplexing via ports and optional error detection via checksum.
- No handshake, no retransmission, no ordering, no flow control, no congestion control — all absent by design.
- The checksum uses 1's complement addition over a pseudo-header plus the UDP segment to detect (not fix) corruption.
- UDP suits latency-sensitive, loss-tolerant, or custom-reliability applications: DNS, VoIP, streaming, gaming, QUIC.
- Lightness is a double-edged sword — it enables speed but pushes reliability and congestion control into the application.
UDP is transport reduced to its irreducible core — ports and an optional checksum — leaving every guarantee of delivery, order, and pacing to the application.
The idea
UDP, specified in RFC 768, is a connectionless transport-layer protocol: sending a UDP datagram requires no prior handshake, no shared state, and no negotiated parameters between sender and receiver. Each datagram is handled independently by the network and by the receiving socket, with no notion of a "connection" persisting between messages. This is in sharp contrast to TCP, which establishes a connection (a three-way handshake), maintains per-connection state (sequence numbers, window sizes, timers), and treats data as a continuous byte stream. UDP is also message-oriented rather than byte-stream-oriented. Each `send()` call from the application produces exactly one UDP datagram, and each successful `recv()` call at the receiver yields exactly one whole datagram — never a partial one, never multiple concatenated ones. This preserves application-level message boundaries, which matters for protocols like DNS where each query and response is a self-contained unit. TCP offers no such guarantee: it may split or coalesce writes arbitrarily. Because UDP has no per-connection state, it is unreliable: datagrams may be lost, duplicated, corrupted, or delivered out of order by the underlying network, and UDP does nothing to detect or correct any of this except for bit-error detection via an optional checksum. It provides no flow control (a fast sender can overwhelm a slow receiver's buffer, causing silent drops) and no congestion control (a UDP sender does not slow down when the network is congested — a property that makes UDP potentially unfair to competing TCP flows, and that motivates congestion control being implemented at the application layer, e.g., inside QUIC). What UDP does guarantee is minimal overhead: an 8-byte header versus TCP's minimum 20 bytes, and no round trip spent on connection setup before the first byte of data can be sent.
How it works
Segment structure
A UDP segment (informally, "datagram") consists of an 8-byte header followed by the application payload: ``` 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 | Destination Port | +-------------------------------+-------------------------------+ | Length | Checksum | +-------------------------------+-------------------------------+ | | | Application Data | | | +----------------------------------------------------------------+ ``` Each field is exactly 16 bits (2 bytes):
- Source Port (16 bits): the sending process's port, used so the receiver can reply. May be all-zero if no reply is expected.
- Destination Port (16 bits): identifies the receiving process/socket on the destination host — this is the field that drives demultiplexing.
- Length (16 bits): the total length in bytes of the UDP header plus data (minimum value 8, for a header with zero-length payload).
- Checksum (16 bits): a 1's-complement checksum over a pseudo-header, the UDP header, and the data, used to detect transmission errors. May be all-zero to indicate "no checksum computed" over IPv4 (mandatory over IPv6).
Demultiplexing
The receiving host's transport layer uses the (destination IP, destination port) pair — with UDP itself needing only the destination port, since UDP is connectionless — to decide which socket's receive buffer gets the datagram. This is connectionless demultiplexing: a socket bound to port `P` receives every incoming datagram addressed to port `P`, regardless of the sender's IP address or port. This differs from TCP's connection-oriented demultiplexing, which uses the full four-tuple (source IP, source port, destination IP, destination port) to route segments to a specific connected socket. ``` DEMULTIPLEX(datagram): dst_port = datagram.header.destination_port socket = lookup_socket_bound_to(dst_port) if socket is None: send_ICMP_port_unreachable(datagram.source) return if socket.receive_buffer.is_full(): drop(datagram) # silent loss — no NACK, no retransmit request return socket.receive_buffer.enqueue(datagram) ```
The checksum algorithm
The checksum is UDP's only defense against corrupted data, and it is optional over IPv4 but effectively always used in practice. It is computed over three things concatenated: a 12-byte pseudo-header (drawn from the IP header: source IP, destination IP, protocol number, UDP length), the UDP header itself (with the checksum field set to zero during computation), and the payload, padded to an even number of bytes with a zero byte if necessary. ``` COMPUTE_CHECKSUM(pseudo_header, udp_header_with_zero_checksum, data): words = split_into_16_bit_words(pseudo_header + udp_header_with_zero_checksum + pad_to_even(data)) sum = 0 for w in words: sum = sum + w while sum > 0xFFFF: # fold any carry back in (end-around carry) carry = sum >> 16 sum = (sum & 0xFFFF) + carry checksum = ones_complement(sum) # bitwise NOT, 16 bits if checksum == 0x0000: checksum = 0xFFFF # avoid confusion with "no checksum" sentinel return checksum VERIFY_CHECKSUM(received_segment): words = split_into_16_bit_words(pseudo_header + full_udp_segment) # checksum field included as-is sum = 0 for w in words: sum = sum + w while sum > 0xFFFF: carry = sum >> 16 sum = (sum & 0xFFFF) + carry result = ones_complement(sum) return result == 0x0000 # all 1s complement of all-1s sum means no detected corruption ``` The key algebraic fact making verification work is that 1's-complement addition of a value and its own complement yields all 1-bits (`0xFFFF` for 16-bit words). If the sender's checksum field is filled with `C = ~(S)` where `S` is the sum of all other words, then the receiver's sum over all words (including `C`) becomes `S + ~S`, which in 1's complement arithmetic equals `0xFFFF`, and its complement is `0x0000`. Any single-bit or odd-parity corruption during transit almost always breaks this identity, so the receiver detects it. It is not perfect: certain patterns of compensating errors (e.g., an error that adds `k` to one word and subtracts `k` from another) can go undetected, which is why checksum is a probabilistic error-detection tool, not a cryptographic guarantee.
Sending path
``` UDP_SEND(app_data, src_port, dst_ip, dst_port): header = build_header(src_port, dst_port, length = 8 + len(app_data)) header.checksum = 0 pseudo = build_pseudo_header(local_ip, dst_ip, PROTO_UDP, header.length) header.checksum = COMPUTE_CHECKSUM(pseudo, header, app_data) segment = header + app_data pass_to_IP_layer(segment, dst_ip) # fire-and-forget: no ack expected, no retry scheduled ``` There is no connection-establishment step, no sequence number, and no retransmission timer armed after this call returns — the application layer, if it needs those guarantees, must implement them itself on top of UDP.
Complexity & correctness
Time complexity of sending/receiving one datagram. Building the header and computing the checksum both take `O(n)` time, where `n` is the datagram length in 16-bit words (i.e., `O(payload size)`), because the checksum algorithm makes a single linear pass, summing each word once with `O(1)` amortized work per word (carries fold in constant time per word since a 32-bit accumulator can absorb at most one carry per addition of two 16-bit values). Demultiplexing is `O(1)` expected time with a hash-table lookup keyed by port number. There is no connection-setup cost (`O(1)` — literally zero handshake messages), in contrast to TCP's `O(1)` messages but nonzero latency (one RTT) for the three-way handshake before data flows.
Space complexity. UDP requires only `O(1)` additional state beyond the socket's receive buffer: a source port, a destination port, and (implementation-dependent) a small queue of unprocessed datagrams per socket. This is far less than TCP, which must retain `O(window size)` bytes of unacknowledged data for potential retransmission, plus per-connection timers, sequence counters, and congestion-control state.
Correctness of the checksum. The claim to verify is: if no error occurs, the receiver's verification always succeeds, and if certain classes of errors occur, verification always fails. The proof rests on properties of 1's complement arithmetic treated as arithmetic modulo `2^16 - 1` (with the caveat that `0` and `2^16-1` are both representable as "all zero bits" and "all one bits" respectively, requiring the "map 0 to all-ones" rule in the checksum-computation step). No-error case. Let `S` be the 1's-complement sum of all words excluding the checksum field, and let `C = onesComplement(S)`. At the receiver, summing all words including `C` gives (in 1's-complement arithmetic) `S + C = S + (M - S) = M`, where `M` represents the all-ones value acting as the additive identity's complement in this number system; concretely, adding a value and its bitwise complement in 1's-complement arithmetic always produces all 1-bits. Complementing all 1-bits yields all 0-bits, so `VERIFY_CHECKSUM` returns true. This shows the protocol has no false positives when the segment is untouched. Error-detection guarantee (partial). If exactly one bit flips anywhere in the covered region, the sum `S'` at the receiver differs from `S`, so `S' + C ≠ 0xFFFF...`, and verification correctly fails — a single bit-flip is always detected. More generally, any odd number of bit-errors within a single 16-bit word is always detected, because such an error changes the parity of that word's contribution to the sum, and the folding/complement arithmetic cannot silently cancel a parity change within one word. The known blind spot is when errors in different words cancel under modular addition (e.g., one word increases by `x` while another decreases by exactly `x`), which is why the checksum is a heuristic, not a cryptographic integrity guarantee — an adversary or extremely unlucky corruption pattern can defeat it. This is the fundamental honest tradeoff: checksums are cheap (`O(n)`, no cryptographic key material) but only probabilistically reliable, unlike a cryptographic MAC.
Correctness of "no guarantees" as a design, not a bug. UDP's specification makes no claim about delivery, order, or duplication, so there is nothing to "prove wrong" — the protocol correctly implements exactly what it promises: best-effort, unordered, unreliable message delivery with optional corruption detection. Any stronger property (ordering, retransmission, congestion response) is, by construction, the application's responsibility, which is the seed of the reliability mechanisms built in Principles of Reliable Data Transfer.
Worked example
Consider a DNS client on port `51234` sending a query to a DNS server on port `53`, with a 4-byte payload `0x0001 0x0001` (a trivially small "message" for illustration — imagine it represents a compressed query ID and flags). We trace the checksum computation by hand.
Setup.
| Field | Value (hex) |
|---|---|
| Source Port | `0xC822` (51234) |
| Destination Port | `0x0035` (53) |
| Length | `0x000C` (12 = 8 header + 4 data) |
| Checksum (zeroed for computation) | `0x0000` |
| Data word 1 | `0x0001` |
| Data word 2 | `0x0001` |
| Pseudo-header source IP | `0x0A000001` (10.0.0.1) |
| Pseudo-header dest IP | `0x0A000002` (10.0.0.2) |
| Pseudo-header zero+protocol | `0x0011` (protocol 17 = UDP) |
| Pseudo-header UDP length | `0x000C` |
Step 1 — list all 16-bit words to sum (pseudo-header, then UDP header with zeroed checksum, then data): ``` 0x0A00 0x0001 0x0A00 0x0002 0x0011 0x000C 0xC822 0x0035 0x000C 0x0000 0x0001 0x0001 ```
Step 2 — sum with end-around carry, accumulating in a wider register and folding overflow back: ``` running = 0x0A00 running += 0x0001 -> 0x0A01 running += 0x0A00 -> 0x1401 running += 0x0002 -> 0x1403 running += 0x0011 -> 0x1414 running += 0x000C -> 0x1420 running += 0xC822 -> 0xD842 running += 0x0035 -> 0xD877 running += 0x000C -> 0xD883 running += 0x0000 -> 0xD883 running += 0x0001 -> 0xD884 running += 0x0001 -> 0xD885 ``` No 16-bit overflow occurred in this particular trace (every partial sum stayed under `0x10000`), so no carry-folding step is triggered here; final sum `S = 0xD885`.
Step 3 — 1's complement. ``` C = ~S = ~0xD885 = 0x277A ``` `C = 0x277A` is not all-zero, so no sentinel substitution is needed. This is the value placed in the Checksum field of the transmitted segment.
Step 4 — receiver verification. The receiver reconstructs the same pseudo-header, then sums all words of the received segment, this time including the actual checksum field `0x277A` instead of `0x0000`: ``` running = 0xD885 (sum of everything except checksum, as before) running += 0x277A -> 0xFFFF ``` `0xFFFF` complemented is `0x0000`, so `VERIFY_CHECKSUM` returns true: no corruption detected, matching the fact that we did not actually corrupt anything in transit. If a single bit had flipped in, say, the data word `0x0001 → 0x0003` during transmission, the receiver's running sum would become `0xD887 + 0x277A = 0x10001`, which folds (carry of `1` added back) to `0x0002`, whose complement `0xFFFD ≠ 0x0000` — verification correctly fails, and the datagram is silently dropped (UDP does not request retransmission; it merely discards it or flags it as errored to the application, depending on OS configuration).
Common pitfalls & traps
| Pitfall | Why it happens | Fix / correct mental model |
|---|---|---|
| Assuming UDP retries on loss | Confusing "transport protocol" with "reliable transport protocol" | UDP never retransmits; reliability, if needed, is entirely the application's job |
| Treating UDP as insecure/broken because it drops packets | Judging UDP by TCP's goals instead of its own | Loss tolerance is intentional — real-time media prefers a dropped frame over a stalled stream |
| Forgetting the pseudo-header in checksum computation | It is not part of the transmitted segment, so it is easy to omit | The pseudo-header (source/dest IP, protocol, length) must be included exactly as spec'd, or checksums won't match across implementations |
| Believing checksum guarantees correctness like a hash/MAC | Both "detect corruption" | Checksum only catches most, not all, errors and has zero protection against malicious tampering |
| Assuming UDP has no minimum overhead vs raw IP | Ports are seen as "free" | UDP still adds 8 bytes per datagram plus per-datagram processing at both ends |
| Using UDP under NAT/firewalls without holepunching | Connectionless services confuse stateful NAT tables | Applications must implement keepalives/STUN-style techniques since there's no "connection" for NAT to track naturally |
| Sending oversized payloads and ignoring fragmentation | UDP has no built-in segmentation to fit MTU | The IP layer will fragment large datagrams, and losing one IP fragment discards the entire UDP datagram |
Pioneers & history
UDP's design descends directly from the same philosophy that shaped the early internet's layered architecture: push complexity to the edges, keep the core simple (the end-to-end argument). It was formalized by David P. Reed, who argued that a bare, unreliable datagram service — distinct from the reliable, connection-oriented service that would become TCP — needed to exist as a first-class transport option, since some applications are better served by minimal transport with reliability logic implemented precisely where it is needed.
Origins · 1 minDavid P. Reed and how this was discovered
Reed was a graduate student and researcher at MIT during the early ARPANET/internet era, contributing foundational work on end-to-end system design. He authored RFC 768, the original one-page specification of UDP (1980), arguing that not every application wants — or should pay for — the overhead of a reliable, ordered, connection-oriented byte stream. His broader work on the end-to-end principle (with Jerome Saltzer and David Clark) shaped how the internet separates concerns between the network core and application-level logic, directly justifying why a protocol as minimal as UDP was worth standardizing alongside TCP rather than forcing every application through one reliability model.
Where this matters
DNS relies on UDP for the vast majority of queries because a single small request-response pair does not justify a TCP handshake's round trip, and the resolver itself implements a simple retry-on-timeout mechanism if a reply never arrives. VoIP and video conferencing tools (RTP over UDP) prefer dropping a stale audio/video frame over waiting for its retransmission, since a late frame is often worse than no frame. Online multiplayer games send frequent, small, loss-tolerant position updates over UDP, layering their own lightweight reliability only on critical events (e.g., "player fired weapon") while letting routine state updates drop freely. DHCP, SNMP, and NTP all use UDP for low-overhead, often broadcast/multicast-friendly communication. Perhaps most significantly, modern web transport is migrating toward QUIC (the transport underlying HTTP/3), which is built entirely on top of UDP specifically to escape kernel-level ossification of TCP and to implement its own multiplexed streams, congestion control, and encryption — a direct, large-scale validation of UDP's original design bet that giving applications a bare, minimal transport primitive would let them build exactly the reliability semantics they actually need. Understanding UDP is therefore a prerequisite for understanding both classic lightweight services and cutting-edge transport redesigns, and it complements the multiplexing concepts introduced in Transport-Layer Services and Multiplexing.
Ask the tutor: How would you design a minimal application-layer acknowledgment scheme on top of UDP to recover just enough reliability for a file-transfer tool, without reimplementing all of TCP?
Why doesn't UDP just add reliability like TCP does?
Because reliability has costs — buffering unacknowledged data, timers, retransmission logic, ordering buffers — that not every application wants to pay. A live audio stream gains nothing from retransmitting a 20ms-old sample; by the time it arrives, playback has already moved past that point in time, so the "reliable" copy is useless. UDP's design philosophy follows the end-to-end argument: since only the application truly knows whether reliability is worth its latency cost, the transport layer should offer a bare mechanism (ports, optional checksum) and let the application implement exactly the reliability semantics it needs, rather than forcing every use case through TCP's one-size-fits -all model.
Does UDP guarantee that datagrams arrive in the order they were sent?
No. Each UDP datagram is routed independently by the underlying IP network, and different datagrams may take different paths with different delays, so they can arrive out of order, be duplicated, or be lost entirely. UDP adds no sequence numbers and performs no reordering at the receiver. If an application needs ordered delivery, it must add its own sequence numbers to the payload and reorder datagrams itself upon receipt, exactly the kind of mechanism explored in reliable-data-transfer protocol design.
Is the UDP checksum mandatory?
Over IPv4 it is optional; a sender may set the checksum field to all-zeros to indicate it computed none, and RFC 768 permits this for performance-sensitive contexts where corruption is either unlikely or is expected to be caught at a higher layer. Over IPv6, the UDP checksum is mandatory (except in specific tunneling scenarios) because IPv6 dropped its own header checksum, making UDP's checksum the only line of defense against a corrupted destination address misdelivering payload data to the wrong host.
How can an application achieve reliability while still using UDP?
By implementing application-layer mechanisms that mirror what TCP does internally: attaching sequence numbers to each datagram, having the receiver send explicit application-level acknowledgments, and having the sender maintain a retransmission timer that resends unacknowledged datagrams after a timeout. QUIC is the most prominent modern example — it runs entirely over UDP but implements its own sequencing, acknowledgment, retransmission, and congestion control, achieving TCP-like reliability while avoiding kernel-level protocol ossification and enabling faster connection establishment and stream multiplexing.
Why is UDP faster to start using than TCP?
TCP requires a three-way handshake (SYN, SYN-ACK, ACK) before the first byte of application data can be sent, costing at least one full round-trip time of pure setup latency. UDP has no connection state to establish at all — the very first datagram sent is application data, arriving as soon as one one-way network transit completes. For latency-sensitive protocols like DNS, where a query-response round trip is already dominated by network transit time, skipping the handshake meaningfully reduces perceived latency, which is why DNS resolvers default to UDP and only fall back to TCP for large responses.
Can two different applications share the same UDP port number?
Not on the same host and same local IP address for receiving purposes — a UDP port must be bound exclusively by one socket to avoid ambiguous demultiplexing, mirroring the same constraint TCP has for listening sockets. However, because UDP demultiplexing uses only the destination port (not the full four-tuple), a single bound UDP socket can receive datagrams from many different remote senders using unrelated source ports and IP addresses, all delivered to that one socket's receive buffer without the OS distinguishing between "connections."
Why does UDP's lack of congestion control matter for network fairness?
TCP flows back off exponentially when they detect congestion (via loss or explicit signals), sharing bandwidth cooperatively with other TCP flows. A UDP application that blasts data at a constant rate regardless of network conditions can starve competing TCP flows sharing the same bottleneck link, since it never yields bandwidth under pressure. This is why responsible UDP-based protocols — QUIC being the flagship example — implement congestion control at the application layer, voluntarily mimicking TCP-friendly behavior, even though nothing in the UDP protocol itself requires them to.