Why this matters
Some programs need every byte, in order, or the result is wrong. A downloaded file with a missing piece cannot be run. The TCP lesson shows how TCP handles this with a handshake, numbered bytes and resends.
Other programs are different. A video call that waits for a lost frame to be sent again is a call where everyone sees a freeze. A live game that waits for an old position update is a game where the other players jump around. For these programs, a small loss is better than a long delay. UDP (User Datagram Protocol) is the transport for this kind of work.
This lesson explains what UDP adds and what it leaves out, then shows the behaviour with short programs you can run. It finishes with the cases where UDP is the right choice, and the cases where it causes trouble. It refers to the TCP lesson for the comparison, so this lesson keeps that comparison short.
The idea: a postcard
Think of a postcard. You write one message, drop it in the post box, and go home. The postcard might arrive in two days, in two weeks, or not at all. Nobody tells you it was lost. If you need an answer, you have to write another postcard and hope.
UDP works this way. Each send is one datagram, meaning one self-contained message with a set size. The sender does not wait for a reply, does not number the datagrams, and does not check that they arrived. The receiver gets whatever arrives, in whatever order it arrives.
UDP still adds two things on top of IP, the layer that moves packets between computers.
- Port numbers. A port is a number from 0 to 65535 that tells the operating system which program a message is for. The clients and servers lesson covers ports in detail. UDP has its own set of ports, separate from TCP's.
- A checksum. A checksum is a small number calculated from the contents of a message. The receiver calculates it again, and if the two numbers differ, the message was damaged in transit and is dropped. A checksum finds damage. It does not fix it, and it does not notice a message that was never sent.
That is all UDP adds. The header, the fixed block of information at the start of each packet, is 8 bytes. A TCP header is at least 20 bytes, and it carries the sequence numbers, acknowledgements and window size that TCP needs.
What UDP leaves out
The table compares the two protocols on the features that matter to a program.
| Feature | TCP | UDP |
|---|---|---|
| Handshake before data | Yes, three messages | No |
| Lost data is sent again | Yes | No |
| Data arrives in order | Yes | No |
| Message boundaries kept | No, data is a stream | Yes, each send is one datagram |
| Header size | At least 20 bytes | 8 bytes |
Each missing feature is a choice. UDP skips the work, so a program that uses UDP gets low delay and light overhead. In exchange, the program must decide for itself what to do when a message is lost, late or out of order. Many programs decide to do nothing, because a newer message will replace the old one anyway.
Worked example 1: a request and a reply, no connection
The simplest UDP program is an echo server. It receives a message and sends back the same message in uppercase. The client sends one datagram and waits for one reply. There is no handshake. The first message from the client is the first message the server sees.
import socket
# UDP echo on localhost: one request, one reply, no handshake.
srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
srv.bind(("127.0.0.1", 9002))
cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cli.settimeout(2)
cli.sendto(b"ping", ("127.0.0.1", 9002))
data, addr = srv.recvfrom(100)
srv.sendto(data.upper(), addr)
print("echo reply:", cli.recvfrom(100)[0])
cli.close()
srv.close()Output:
echo reply: b'PING'Three things to notice.
SOCK_DGRAMis the socket type for UDP.SOCK_STREAM(used in the TCP lesson) is the type for TCP. The name tells you what the socket promises: datagrams, or a stream.sendtotakes the destination address with each send. A UDP socket does not have a connection to a particular peer, so every message must say where it goes.recvfromreturns the message and the address of the sender, which is how the server knows where to reply.- The client needs no
connectstep. The first message goes out immediately. This is where UDP saves time: one round trip for a request and its reply, with no handshake in front.
Worked example 2: each send is one message
A TCP stream merges writes. A UDP datagram keeps its edges. The next test sends three messages in a row, and the receiver reads them both ways. It uses a pair of local sockets so that the result depends only on the stream or datagram rules.
import socket
# Stream vs datagram: TCP merges writes, UDP keeps each send as one message.
a, b = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
a.sendall(b"one"); a.sendall(b"two"); a.sendall(b"three")
print("TCP-style stream recv:", b.recv(100))
a.close(); b.close()
a, b = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
a.send(b"one"); a.send(b"two"); a.send(b"three")
print("datagram recv:", [b.recv(100) for _ in range(3)])
a.close(); b.close()Output:
TCP-style stream recv: b'onetwothree'
datagram recv: [b'one', b'two', b'three']The stream version returned one blob, with no sign of the three sends. The datagram version returned three separate messages. A program that uses UDP can rely on this. A program that uses TCP cannot, and must mark its own message boundaries. The socket pair here is a local stand-in, but the rule matches the real UDP and TCP sockets used in the rest of this lesson.
The UDP socket also has one more property. A datagram larger than one network packet is split into pieces, and losing any one piece loses the whole datagram. A receive call with a buffer that is too small gets only the start of the datagram, and the rest is thrown away. Many programs keep each datagram small, often under about 1,200 to 1,400 bytes, so it fits in one packet on most networks. Larger messages need the program to split them and rebuild them itself.
Worked example 3: sending into nothing
UDP does not tell the sender when a message fails. The next test sends to a port with no program listening, first with sendto, and then with a connected socket.
import socket
# UDP to a closed port: sending succeeds, nobody answers.
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
u.sendto(b"anyone there?", ("127.0.0.1", 9))
print("sendto returned without error")
# A connected UDP socket does report the ICMP "port unreachable" on its next call.
u.connect(("127.0.0.1", 9))
u.send(b"anyone there?")
try:
u.recv(100)
except OSError as e:
print(type(e).__name__, "-", e)
u.close()Output:
sendto returned without error
ConnectionRefusedError - [Errno 111] Connection refusedThe first sendto returned without an error. The message was sent into the network with no check that anyone would receive it. The receiving computer, on finding no program on that port, sent back an ICMP message (a control message used by the network layer, which says "port unreachable"). A plain UDP socket ignores that message. A socket that has called connect gets the message as an error on its next read or send, and the error text is the same wording the TCP refused error uses.
The lesson for programs is that sendto returning without an error means only that the message left the sender. It says nothing about whether the other side got it.
Worked example 4: a fast stream loses messages
The next program sends 2,000 datagrams to a receiver that does not read while they are sent. The receiver has a small buffer, which is the memory the operating system keeps for incoming data until the program reads it. When the buffer is full, new datagrams are dropped, and nothing tells the sender.
import socket, time
# Receiver with a small buffer that does not read while the sender runs.
rx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rx.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16 * 1024)
rx.bind(("127.0.0.1", 9003))
rx.settimeout(0.5)
tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sent = 2000
for i in range(sent):
tx.sendto(f"{i:04d}".encode() + b"x" * 996, ("127.0.0.1", 9003))
time.sleep(0.2)
got = []
try:
while True:
data, _ = rx.recvfrom(2048)
got.append(int(data[:4]))
except socket.timeout:
pass
print("sent:", sent, "received:", len(got))
print("first 5 received numbers:", got[:5])
print("last received number:", got[-1] if got else None)
print("missing count:", sent - len(got))
tx.close()
rx.close()Output from one run:
sent: 2000 received: 14
first 5 received numbers: [0, 1, 2, 3, 4]
last received number: 13
missing count: 1986The first 14 datagrams fit in the buffer. The other 1,986 were dropped, and the sender never heard about it. The number of datagrams that survive changes from run to run, because it depends on how fast the operating system moves data, but the pattern is the same: a sender that is faster than the receiver loses data, and UDP does not slow it down.
Real networks lose datagrams for other reasons too, such as a busy link or a router that runs out of space. The program above shows the cause on one machine, and it is the same loss a program sees on a network.
Where UDP fits
UDP is the right choice when a late message is worth less than a fast one, or when a message is so short that a retry is cheap. Three examples show how to decide.
DNS lookups. A DNS lookup is one short question and one short answer. The DNS lesson describes how a name becomes an address. The DNS client sends a question as a single UDP datagram and waits for the reply. If no reply comes back in time, the client asks again itself. The next example sends a real DNS question over UDP to the local resolver on this machine, a service at 127.0.0.53 that listens for DNS queries. The query is built by hand, so you can see the bytes.
import socket, struct
def build_query(name, txid=0x1234):
header = struct.pack(">HHHHHH", txid, 0x0100, 1, 0, 0, 0) # recursion desired, 1 question
qname = b"".join(bytes([len(p)]) + p.encode() for p in name.split(".")) + b"\x00"
return header + qname + struct.pack(">HH", 1, 1) # type A, class IN
query = build_query("example.com")
print("query size in bytes:", len(query))
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(3)
s.sendto(query, ("127.0.0.53", 53))
try:
reply, addr = s.recvfrom(512)
txid, flags, qd, an, ns, ar = struct.unpack(">HHHHHH", reply[:12])
print("reply from:", addr, "size:", len(reply))
print("transaction id matches:", txid == 0x1234)
print("response bit set:", bool(flags & 0x8000), "rcode:", flags & 0xF)
print("answers:", an)
except socket.timeout:
print("no reply within 3 seconds")
finally:
s.close()Output from one run:
query size in bytes: 29
reply from: ('127.0.0.53', 53) size: 61
transaction id matches: True
response bit set: True rcode: 0
answers: 2The whole exchange is two datagrams: 29 bytes out and 61 bytes back. The transaction id (a number the client picks, which the server copies into its reply) lets the client match the reply to the question. The response bit is set in the reply, and rcode 0 means no error. A TCP version of this lookup would need a handshake, which costs a round trip before the question is sent.
Video calls. A video call sends a stream of small pictures, usually called frames, many times per second. A frame that arrives 300 milliseconds late is no longer useful, because the call has moved on. Resending it would add delay to every frame that follows. Many video call apps therefore send media over UDP, and when a frame is missing, they show a short glitch or repeat the previous picture. Some real-time systems add their own light checks on top of UDP, but they do not wait for old data.
Live games. A game client may send the player's position many times per second. The newest position is the one that matters. An old position that arrives late tells the server where the player used to be, and nothing else. UDP lets the newest position replace the old one without waiting for a resend.
Not every program that uses UDP is a simple "fire and forget" design. QUIC is a transport built on top of UDP that adds its own reliability, encryption and ordering inside the program. It is used for the newest version of HTTP. The idea is the same: UDP provides the sending, and the program adds only the parts it needs.
What you give up without retries and ordering
Choosing UDP means accepting four things. Each one can cause a bug if the program does not plan for it.
- Lost messages stay lost. Nothing sends them again. If a program needs a message to arrive, it must ask for it again, or the state it tracks will drift from what the other side knows.
- Messages can arrive out of order. A message sent first may arrive after one sent later. A program that applies each message as it arrives may apply an old change after a new one.
- There is no flow control. TCP slows the sender when the receiver is full. UDP does not, so a fast sender can overload a slow receiver, as the 2,000-datagram test showed.
- There is no congestion control on the path. TCP also slows down when the network is busy, and speeds up again when it is clear. A UDP program that sends at full speed on a busy network makes the busy network worse for everyone. Programs that use UDP for real traffic add their own rate limits for this reason.
A program can add the parts it needs. The simplest is a sequence number on each message. The receiver checks the numbers and notices gaps, so it can ask for a resend or decide to ignore the missing data. The next script simulates the check on a list of arrival numbers, where message 4 arrives late.
# Each message starts with a sequence number. The receiver looks for gaps.
received = [0, 1, 2, 3, 7, 8, 10, 4] # message 4 arrived late, after 7 and 8
seen = set()
highest = -1
for n in received:
seen.add(n)
if n > highest + 1:
missing = [m for m in range(highest + 1, n) if m not in seen]
print(f"after message {n}: gap, numbers {missing} not seen yet")
highest = max(highest, n)
print("highest number:", highest)
print("missing so far:", [m for m in range(highest + 1) if m not in seen])Output:
after message 7: gap, numbers [4, 5, 6] not seen yet
after message 10: gap, numbers [9] not seen yet
highest number: 10
missing so far: [5, 6, 9]Message 4 is reported as a gap at first, because it has not arrived yet when message 7 is seen. The check cannot tell a late message from a lost one when the gap first appears. A program that uses UDP therefore picks a waiting time for its gaps, and only treats a number as lost after that time passes.
Common mistakes
1. Using UDP to download a file because it is faster. The download finishes, but parts are missing. The file looks complete and fails when opened. A file transfer needs every byte, so it needs TCP or a program that adds its own resends. Keep UDP for data where losing some of it is acceptable.
2. Treating a successful sendto as delivery. The first test in worked example 3 showed the sender returning without an error for a port that was not listening. A success from sendto means only that the message left the sender.
3. Forgetting that a connected UDP socket reports old errors later. A socket that has called connect can raise a refused error on a later call, long after the send that caused it:
ConnectionRefusedError - [Errno 111] Connection refusedThe error does not mean the current send failed. It reports that an earlier datagram reached a closed port. A program that stops at the first error may give up too early.
4. Waiting for a reply with no timeout. If the reply never comes, recv waits forever. Set a timeout on every receive, and decide what the program does when it runs out. The examples in this lesson use settimeout for that reason.
TimeoutError - timed outThis is the error Python raises when the timeout passes with no reply. The fix is a retry, a fallback or a clear error to the user.
5. Assuming the order of replies matches the order of sends. A program that sends three questions and reads three replies must not assume the first reply answers the first question. Each reply should carry an identifier, like the transaction id in the DNS example, so the program can match them.
Practice
Exercise 1. A chat app sends each message as a UDP datagram. A user types "yes" and the datagram is lost. What does the user see, and what would you change?
Show the answer
The user sees nothing happen, and the sender has no sign that the message was lost. The app needs a way to know the message did not arrive. It can add a message id to each message, wait for an acknowledgement, and resend if none comes in time. That is the work TCP does, which is why chat apps often use TCP instead.
Exercise 2. A UDP program sends 100 messages. The receiver gets numbers 0 to 9, then 12, 13, then 10. Which numbers are missing, and which are late?
Show the answer
Number 11 is missing so far, because it has not arrived. Numbers 12 and 13 arrived before 10, so the arrivals are out of order. Number 10 is late, not lost, because it did arrive. If 11 never arrives, the program can decide to ignore it, since the next messages carry newer positions.
Exercise 3. Why does the DNS query in this lesson take one round trip to finish, while a TCP-based request to the same server would need more time?
Show the answer
The UDP query sends one datagram and gets one reply, which is one round trip. A TCP request needs the handshake first, so it needs one round trip for the handshake and one more for the request and reply. The TCP lesson shows this extra round trip.
Exercise 4. The 2,000-datagram test received 14 messages. Name two changes that would make the receiver get more of them, and say what each one costs.
Show the answer
The receiver could read the buffer faster, by using a thread or a more efficient loop, so the buffer does not fill. This costs more code. The receiver could also use a bigger buffer with SO_RCVBUF, which gives more room before messages drop. This costs memory, and it only helps for short bursts, because a sender that keeps sending faster than the receiver reads still fills any buffer in the end.
Exercise 5. For each of these, say whether UDP or TCP fits better, and give one reason: a bank transfer, a live sports score ticker that updates every second, and a photo upload.
Show the answer
A bank transfer needs TCP, because every message must arrive and in order. A live score ticker can use UDP, because each update replaces the last one and an old score is useless. A photo upload needs TCP, because a photo with missing bytes cannot be opened.
Summary
- UDP sends each message once, with no handshake, no resends and no ordering. It adds only port numbers and a checksum on top of IP.
- A UDP datagram keeps its edges. One send is one message to the receiver, while a TCP stream merges sends together.
sendtoreturning without an error does not mean the other side got the message. A connected socket may report an earlier failure later.- UDP fits lookups like DNS, video calls and live games, where a late message is worth less than a fast one.
- A UDP program that needs reliability must add its own sequence numbers, timeouts and resends, or accept the loss.
- Without flow control, a fast sender can drop data at a slow receiver, and nothing tells the sender.