Build the mental model
It helps to stop thinking of UDP as 'TCP with the good parts removed' and start thinking of it as the thinnest possible layer that adds port numbers to IP. UDP sets up no connection, performs no handshake, keeps no sequence numbers, never retransmits, never reorders, and does neither flow control nor congestion control. It supplies a source port, a destination port, a length and a checksum — the bare minimum needed to get a datagram to the right process on the right host.
Those omissions are features because every guarantee TCP offers is bought with time. When a segment is lost, TCP retransmits it, but meanwhile it holds back all the data that already arrived behind the gap rather than passing it to the application. That is head-of-line blocking. In a voice call, pausing all subsequent audio to recover a frame from 200 ms ago is the genuinely bad outcome; dropping that frame is far better. In a game, a stale position update is worthless anyway because a fresher one has already arrived. A DNS query fits in one datagram, so paying for a three-way handshake to carry it makes no sense.
The important nuance is that choosing UDP is not giving up reliability — it is taking control of what reliability means for your application. QUIC does exactly this. It runs on UDP and rebuilds ordering, retransmission and congestion control in user space, but per stream, so a loss in one stream no longer blocks the others. It sits on UDP precisely because that lets a brand-new transport design be deployed without changing every kernel's TCP stack or every middlebox on the path.
UDP HEADER VERSUS TCP HEADER, FIELD BY FIELD
--------------------------------------------
UDP HEADER (8 bytes) TCP HEADER (20 bytes min)
0 31 0 31
+----------+----------+ +-------------+-------------+
| src port | dst port | | src port | dst port |
+----------+----------+ +-------------+-------------+
| length | checksum | | sequence number |
+----------+----------+ +---------------------------+
| acknowledgement number |
that is the ENTIRE header. +------+------+-------------+
no seq, no ack, no window, | off | flag | window |
no state, no retransmit, +------+------+-------------+
no ordering, no handshake. | checksum | urgent ptr |
+-------------+-------------+Connect it to a real scenario
Suppose you are designing a video-call feature. The question to ask is not 'do I need reliability' but 'how long until this piece of data is worthless'. An audio frame represents 20 ms of sound and the player can buffer perhaps 60 ms of jitter. If a frame is lost, retransmitting it costs a full round trip; with a 120 ms RTT the replacement arrives long after its playback slot has passed, so it is useless. Sending over UDP and letting the codec's packet-loss concealment paper over the gap is the correct design.
But notice that requirements differ even within one feature. The signalling message that says 'start the call', the chat messages, and the participant list must not be lost, so they belong on TCP or TLS. Most real applications use both: TCP for the control plane, UDP for the media. The other thing UDP hands you is a responsibility. Because nothing underneath is backing off, an application that keeps blasting datagrams into a congested path damages its own quality and everyone else's traffic sharing that link. If you choose UDP, you own pacing and congestion response — which is precisely the work QUIC did once so that applications built on it do not have to redo it.
Try the working example
import struct
# ---- Build a UDP datagram: 8-byte header + payload ----------------------
payload = b"hello over udp"
src_port = 49152
dst_port = 5353
length = 8 + len(payload) # UDP length COUNTS the header itself
checksum = 0 # 0 = "not computed", legal in IPv4 UDP
udp_header = struct.pack("!HHHH", src_port, dst_port, length, checksum)
datagram = udp_header + payload
print("UDP header hex :", udp_header.hex())
print("header bytes :", len(udp_header))
print("datagram bytes :", len(datagram))
# ---- Parse it back ------------------------------------------------------
s, d, ln, csum = struct.unpack("!HHHH", datagram[:8])
body = datagram[8:ln]
print("parsed ports : %d -> %d" % (s, d))
print("parsed length : %d (payload %d bytes)" % (ln, ln - 8))
print("parsed payload :", body.decode())
print("checksum : 0x%04x" % csum)
# ---- Header cost compared with TCP -------------------------------------
print()
print("field UDP TCP")
rows = [
("source port", 2, 2),
("destination port", 2, 2),
("length", 2, 0),
("checksum", 2, 2),
("sequence number", 0, 4),
("acknowledgement number", 0, 4),
("offset + flags", 0, 2),
("receive window", 0, 2),
("urgent pointer", 0, 2),
]
for name, u, t in rows:
print("%-24s %5s %5s" % (name, u or "-", t or "-"))
udp_bytes = sum(r[1] for r in rows)
tcp_bytes = sum(r[2] for r in rows)
print("%-24s %5d %5d" % ("TOTAL", udp_bytes, tcp_bytes))
for size in (64, 1200):
u_over = 100.0 * udp_bytes / (udp_bytes + size)
t_over = 100.0 * tcp_bytes / (tcp_bytes + size)
print("overhead on %4d B of data: UDP %.1f%% TCP %.1f%%"
% (size, u_over, t_over))UDP header hex : c00014e900160000
header bytes : 8
datagram bytes : 22
parsed ports : 49152 -> 5353
parsed length : 22 (payload 14 bytes)
parsed payload : hello over udp
checksum : 0x0000
field UDP TCP
source port 2 2
destination port 2 2
length 2 -
checksum 2 2
sequence number - 4
acknowledgement number - 4
offset + flags - 2
receive window - 2
urgent pointer - 2
TOTAL 8 20
overhead on 64 B of data: UDP 11.1% TCP 23.8%
overhead on 1200 B of data: UDP 0.7% TCP 1.6%5-minute try-it
Change the payload to 1 byte and then to 1400 bytes and compare the overhead percentages — at which payload size does the TCP/UDP header difference stop mattering? Then corrupt the length field by hand and observe exactly how the parser mis-slices the payload.
One important caution
Assuming UDP is 'faster' and using it for bulk transfer. On a lossy path you end up writing your own retransmission logic and rebuilding a worse TCP.
Forgetting that an oversized datagram gets IP-fragmented, and losing any one fragment destroys the whole datagram. Keep payloads under the path MTU, commonly around 1200 bytes.
RFC 768 - User Datagram Protocol — Computer Networking