TCP vs UDP
Two transport-layer protocols. TCP is reliable and connection-oriented. UDP is fast and best-effort. Almost every Python backend you’ve ever written uses TCP (HTTP, gRPC, Postgres, Redis). UDP shows up for DNS, NTP, video/voice, and observability (Statsd, syslog, traces).
Side-by-side
| TCP | UDP | |
|---|---|---|
| Connection | yes (3-way handshake) | no (just send a packet) |
| Reliability | guaranteed delivery + order | none |
| Retransmission | yes | no (app must do it) |
| Flow control | yes (window) | no |
| Congestion control | yes | no |
| Header size | 20 bytes | 8 bytes |
| Use cases | HTTP(S), SSH, DBs, gRPC | DNS, NTP, VoIP, video, metrics, QUIC base |
| Latency | higher (handshake + ACKs) | lower |
| “Stream” or “message”? | byte stream | discrete datagrams |
TCP three-way handshake
Client ──── SYN ────▶ Server
Client ◀── SYN+ACK ── Server
Client ──── ACK ────▶ Server
( connection established )
Cost: 1 RTT before any data flies. For a Python service in us-east-1 calling another in eu-west-1 (~80ms RTT), every new TCP connection burns 80ms. This is why connection pooling (requests.Session, httpx.AsyncClient, DB pools) matters.
TCP four-way close
Client ── FIN ──▶ Server
Client ◀── ACK ── Server
Client ◀── FIN ── Server
Client ── ACK ──▶ Server
After the close, the initiating side stays in TIME_WAIT for ~2× MSL (60–120s) to absorb stragglers. Lots of TIME_WAIT sockets on a busy short-connection service is a real production issue — fix with keepalive / pooling.
When UDP makes sense
- You can tolerate loss (a dropped audio frame is better than a 200ms stall to retransmit).
- Latency dominates throughput. No handshake means the first byte arrives faster.
- Many small messages. TCP’s per-stream state is overhead.
- Your protocol does its own reliability. QUIC (HTTP/3) is UDP underneath because TCP head-of-line blocking hurts multiplexed streams.
QUIC / HTTP/3 — why UDP came back
TCP has a head-of-line problem with multiplexed streams (HTTP/2 puts 100 requests on one TCP connection; one lost packet stalls them all). QUIC reimplements reliability + ordering on UDP, per stream, so a lost packet only stalls its own stream. HTTP/3 is HTTP over QUIC over UDP. Your Python code doesn’t notice unless you specifically opt in.
TCP states (netstat -ant lingo)
| State | Means |
|---|---|
LISTEN |
server socket waiting for connections |
ESTABLISHED |
open connection, data flowing |
SYN_SENT / SYN_RECV |
mid-handshake |
FIN_WAIT_1, FIN_WAIT_2, CLOSE_WAIT |
half-closed, waiting on the other side |
TIME_WAIT |
post-close, draining stragglers |
CLOSED |
gone |
In production debugging:
- Lots of
TIME_WAITon a client → too many short connections, missing keep-alive. - Lots of
CLOSE_WAITon a server → app isn’t callingclose()on its side. SYN_SENTaccumulating → can’t reach the server (firewall, dropped packets).
Python sockets
import socket
# TCP server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", 9000))
s.listen(128)
conn, addr = s.accept()
data = conn.recv(4096)
# UDP server
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
u.bind(("0.0.0.0", 9001))
data, addr = u.recvfrom(4096)
Note SOCK_STREAM (TCP) gives you recv (a stream — you may get partial messages and have to buffer); SOCK_DGRAM (UDP) gives you recvfrom (one packet per call, with the sender address).
TCP gotcha: recv() doesn’t give you “one message”
conn.recv(4096) returns up to 4096 bytes, but could give you 1, 50, or all of them — TCP is a byte stream, not a message protocol. You need a length prefix or delimiter to frame messages. This bites people writing custom TCP protocols in Python; HTTP and gRPC handle it for you.
Common interview confusions
- “Does HTTPS use TCP?” — yes (HTTP/1 and HTTP/2). HTTP/3 uses UDP via QUIC.
- “Why isn’t gRPC over UDP?” — it’s HTTP/2 (TCP). gRPC over QUIC exists but isn’t mainstream yet.
- “Is DNS TCP or UDP?” — both. UDP for normal queries (under 512 bytes), TCP for zone transfers and oversized responses (DNSSEC etc.).
- “Does UDP have ports?” — yes. Same 0–65535 port space conceptually, but separate from TCP — UDP/53 (DNS) and TCP/53 (DNS zone transfer) are different sockets.
Interview angle
- “When would you choose UDP over TCP?” — loss-tolerant low-latency traffic (voice/video, metrics, DNS), or when you build your own reliability on top (QUIC). Default is TCP.
- “What’s the cost of opening a new TCP connection vs reusing one?” — 1 RTT for the handshake, plus TLS adds 1–2 more RTTs. With 80ms RTT that’s 240ms before the first HTTP byte. Pool connections to avoid this.
- “What’s
TIME_WAITand why does it matter?” — post-close state that holds the socket for ~2× MSL. Too many on a busy client = port exhaustion. Fix with keepalive, pooling, orSO_REUSEADDR/SO_REUSEPORT. - “Why does HTTP/3 use UDP?” — TCP’s head-of-line blocking hurts HTTP/2 multiplexed streams. QUIC moves reliability into the protocol itself, per-stream, on UDP.
- “Two Python processes communicate over TCP — what could go wrong with
recv(4096)?” — TCP is a byte stream.recvcan return partial messages or multiple messages glued together. You must frame with a length prefix or delimiter.