10 Nov 2026 · redes, linux, tcp, udp · 4 min
TCP and UDP in practice: when the difference shows
Answer paragraph: TCP and UDP differ in one thing only: TCP resends what was lost, UDP does not. With 5% simulated loss on a 5 MB transfer, TCP took 5.6 seconds instead of 2.0 and made 5 to 7 retransmissions; UDP lost 4.5% of the packets and did not notice. Choose TCP when the data must arrive intact and UDP when delay costs more than loss.
What each one does
TCP opens a connection, numbers every byte, and acknowledges receipt. If a packet is lost, the sender finds out from the missing acknowledgment and resends it. UDP sends datagrams and moves on; there is no acknowledgment, no resend, and no guaranteed order.
The difference shows up when the link is not perfect. On a clean link, both deliver the same file in the same time. With loss, TCP holds the flow until it retransmits, and UDP moves on without what is missing.
The measurement below uses 5% loss, high for a local network but common on a distant wireless link.
Measurement
A 5 MB file (5,242,880 bytes) was sent over TCP and UDP on the loopback of a container, with and without loss simulated by tc netem. Without loss, TCP took 2.0 s and UDP took 2.0 s. With 5% loss and 50 ms delay, TCP took 5.6 s with 5 to 7 retransmissions; UDP lost 45 of 1000 packets (4.5%) and finished in 2.0 s.
| scenario | TCP time | TCP retransmissions | UDP packets received |
|---|---|---|---|
| no loss | 2.0 s | 0 | 1000 of 1000 |
| 5% loss, 50 ms | 5.6 s | 5 to 7 | 955 of 1000 |
TCP retransmission is the price of complete delivery. UDP does not pay that price, but it also does not deliver what is missing.
The artifact
The script below builds an isolated lab in a container, with netns and tc netem, and runs both measurements. It requires root and the packages iproute2 and netcat-openbsd.
#!/usr/bin/env bash
# medir-tcp-udp.sh
set -euo pipefail
if ! command -v tc >/dev/null 2>&1 || ! command -v nc >/dev/null 2>&1; then
echo "dependências ausentes: instale iproute2 e netcat-openbsd"
echo "FIM"
exit 0
fi
PAYLOAD=/tmp/payload.bin
head -c 5242880 /dev/urandom > "$PAYLOAD"
get_retrans() { grep '^Tcp:' /proc/net/snmp | tail -1 | awk '{print $13}'; }
medir_tcp() {
local netem="$1"
tc qdisc del dev lo root 2>/dev/null || true
if [ "$netem" = "sim" ]; then
tc qdisc add dev lo root netem loss 5% delay 50ms
fi
rm -f /tmp/tcp_recv.bin
nc -l -p 9000 > /tmp/tcp_recv.bin &
local rpid=$!
sleep 0.5
local r0=$(get_retrans)
local start=$(date +%s%N)
nc -q 2 127.0.0.1 9000 < "$PAYLOAD"
local end=$(date +%s%N)
wait "$rpid" 2>/dev/null || true
local r1=$(get_retrans)
local recv=$(stat -c %s /tmp/tcp_recv.bin 2>/dev/null || echo 0)
printf 'TCP %s: enviados=%s recebidos=%s tempo_ms=%s retransmissoes=%s\n' \
"$netem" "$(stat -c %s "$PAYLOAD")" "$recv" "$(( (end-start)/1000000 ))" "$((r1-r0))"
}
medir_udp() {
local netem="$1"
tc qdisc del dev lo root 2>/dev/null || true
if [ "$netem" = "sim" ]; then
tc qdisc add dev lo root netem loss 5% delay 50ms
fi
python3 - "$netem" <<'PY'
import socket, time, sys
netem = sys.argv[1]
n = 1000
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
count = 0
recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
recv.bind(("127.0.0.1", 9002))
recv.settimeout(3)
import threading
def listener():
global count
deadline = time.time() + 6
while time.time() < deadline:
try:
recv.recvfrom(2048)
count += 1
except socket.timeout:
break
t = threading.Thread(target=listener, daemon=True)
t.start()
time.sleep(1)
for _ in range(n):
sock.sendto(b"x" * 100, ("127.0.0.1", 9002))
time.sleep(0.002)
t.join()
print(f"UDP {netem}: enviados={n} recebidos={count}")
PY
}
medir_tcp "nao"
medir_tcp "sim"
medir_udp "nao"
medir_udp "sim"
tc qdisc del dev lo root 2>/dev/null || true
echo "FIM"
How to verify
Run the script in a container with --cap-add=NET_ADMIN. The expected output has four lines, the last one FIM:
TCP nao: enviados=5242880 recebidos=5242880 tempo_ms=2009 retransmissoes=0
TCP sim: enviados=5242880 recebidos=5242880 tempo_ms=5627 retransmissoes=5
UDP nao: enviados=1000 recebidos=1000
UDP sim: enviados=1000 recebidos=955
FIM
If the retransmission count varies between runs, that is normal: netem uses a random seed.
If it goes wrong
tc: Operation not permitted: the container needs --cap-add=NET_ADMIN. Without that capability, netem does not apply and the measurement becomes a no-loss test.
nc: command not found: install netcat-openbsd. The nc from netcat-traditional has a different syntax for -q.
python3 without the socket module: unlikely; socket is part of the standard library. If the interpreter is a minimal build, use the distribution’s python3.
When to use which
Use TCP for files, APIs, databases, SSH, and any data that must arrive intact. Use UDP for voice, live video, and games, where a late packet is worth less than a timely one. DNS uses UDP by default and falls back to TCP when the response does not fit in a datagram.
| protocol | delivery | order | retransmission | typical case |
|---|---|---|---|---|
| TCP | guaranteed | guaranteed | yes | file, API, SSH |
| UDP | best effort | not guaranteed | no | voice, video, game |
FAQ
Is TCP always slower than UDP? No. On a clean link both deliver in the same time. The difference shows up with loss or congestion.
Does UDP lose packets without telling you? The protocol does not tell you. The application can implement acknowledgment on top, as QUIC does over UDP.
Can you simulate loss without root? Not with tc. There are user-space tools, but they are out of scope here.
Why did TCP retransmit only 5 times for 5% loss? Because the loss was 5% of packets, and the retransmission algorithm groups bytes into segments. The exact number varies per run.
Is it worth reproducing the measurement? Yes, if you are choosing a protocol for a service you run. The measurement takes less than a minute in a disposable container.
Sources
Internal links: see also what a container really is and processes, ports and connections.