Computer Networks Lesson 2 of 6 ⏱️ 5 min read

TCP vs UDP: Handshakes, Reliability & Sockets

The TCP three-way handshake, reliability and flow control, when UDP is the better choice, and socket programming in Python.

Updated Oct 30, 2026

Two transport protocols

The transport layer delivers data between processes (identified by ports) on two hosts. The internet's two classic transport protocols take opposite approaches:

TCP UDP
Connection Connection-oriented (handshake first) Connectionless
Reliability Retransmits lost data, guarantees delivery or an error No guarantees — packets may be lost
Ordering Delivers bytes in order Datagrams may arrive out of order
Data model Byte stream Individual datagrams (messages)
Flow/congestion control Yes No
Header size 20+ bytes 8 bytes
Speed / latency More overhead Minimal overhead
Used by HTTP/1.1, HTTP/2, SSH, databases, email DNS, video calls, online games, VoIP, QUIC (HTTP/3)

The TCP three-way handshake

Before sending data, TCP establishes a connection and agrees on starting sequence numbers:

Plaintext
Client                                   Server
  │ ── SYN (seq = x) ─────────────────────▶ │   "I want to connect"
  │ ◀──────────── SYN-ACK (seq = y, ack = x+1) │   "OK, I'm ready too"
  │ ── ACK (ack = y+1) ───────────────────▶ │   "Great, let's talk"
  │ ═════════════ data flows ═════════════ │

The handshake costs one round trip before any data is sent — which is why connection reuse (HTTP keep-alive, connection pools) matters so much for performance. With TLS on top, setup takes another 1 RTT (TLS 1.3).

Closing a connection uses FIN and ACK messages from each side. The side that closes first enters TIME_WAIT for a while, so busy servers making many short outbound connections can run out of ports.

How TCP makes delivery reliable

  • Sequence numbers — every byte is numbered so the receiver can reorder and detect gaps.
  • Acknowledgements (ACKs) — the receiver confirms what it got.
  • Retransmission — data not acknowledged within a timeout is sent again.
  • Checksums — detect corrupted segments.
  • Flow control — the receiver advertises a window of how much it can accept, so a fast sender doesn't overwhelm a slow receiver.
  • Congestion control — algorithms such as CUBIC and BBR slow down when the network is congested ("slow start", then grow carefully).

A side effect: head-of-line blocking. If one segment is lost, later bytes wait in the buffer until it's retransmitted — even if they belong to unrelated requests. HTTP/3 moves to QUIC over UDP partly to avoid this.

When UDP is the better choice

UDP just sends datagrams. That's ideal when:

  • Late data is useless — in a video call, a retransmitted frame from 300 ms ago is worse than skipping it.
  • Messages are tiny and independent — a DNS query fits in one datagram; a handshake would double the latency.
  • You want to build your own reliability — QUIC implements streams, encryption and retransmission on top of UDP.
  • Broadcast or multicast is needed (e.g. local service discovery).

Sockets: TCP in Python

A socket is the programming interface to the transport layer. Here's a TCP echo server and client on your own machine:

Python
import socket
import threading

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   # SOCK_STREAM = TCP
server.bind(("127.0.0.1", 0))                                # port 0 = pick any free port
server.listen()
port = server.getsockname()[1]

def serve_one():
    conn, addr = server.accept()              # completes the three-way handshake
    with conn:
        data = conn.recv(1024)
        conn.sendall(b"echo: " + data)

threading.Thread(target=serve_one).start()

with socket.create_connection(("127.0.0.1", port)) as client:
    client.sendall(b"hello over TCP")
    print(client.recv(1024).decode())
server.close()

Output:

Plaintext
echo: hello over TCP

Note that TCP is a byte stream: one sendall isn't guaranteed to arrive as one recv. Real protocols add framing — a length prefix or a delimiter such as \r\n (HTTP/1.1 headers) — so the receiver knows where messages end.

Sockets: UDP in Python

UDP has no listen/accept — you just send datagrams to an address:

Python
import socket

receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # SOCK_DGRAM = UDP
receiver.bind(("127.0.0.1", 0))
address = receiver.getsockname()

sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for i in range(3):
    sender.sendto(f"reading #{i}".encode(), address)        # no handshake, no connection

for _ in range(3):
    data, source = receiver.recvfrom(1024)                 # each recvfrom = one whole datagram
    print(data.decode())

sender.close(); receiver.close()

Output:

Plaintext
reading #0
reading #1
reading #2

On localhost nothing gets lost. Across a real network, some datagrams could vanish or arrive out of order, and the application would have to cope.

Common ports to know

Port Service
22 SSH
53 DNS (UDP, and TCP for large responses)
80 / 443 HTTP / HTTPS (443 also UDP for HTTP/3)
3306 / 5432 MySQL / PostgreSQL
6379 Redis
9092 Kafka

Common mistakes

  • Assuming one send equals one recv in TCP — always frame messages.
  • Opening a new connection per request — reuse connections and pools.
  • Saying UDP is "unreliable so never use it" — HTTP/3, DNS and real-time media depend on it.
  • No timeouts on sockets — a silent peer can hang your worker forever.

Key takeaways

  • TCP: connection-oriented, reliable, ordered byte stream with flow and congestion control.
  • UDP: connectionless datagrams with minimal overhead and no delivery guarantees.
  • The three-way handshake (SYN, SYN-ACK, ACK) costs one round trip — reuse connections.
  • Choose UDP when timeliness beats completeness, or when building a custom protocol like QUIC.
Finished this lesson?
Tick it off to track your Computer Networks progress.
Next lesson