One Network, Two Philosophies
The transport layer delivers data between two processes (identified by ports) on top of the best-effort network layer. IP alone does not guarantee delivery, ordering, or integrity — so TCP exists to add those guarantees, and UDP exists to skip them for speed. Which one you choose is a semantic decision: “I need a reliable byte stream” → TCP; “I need low latency / real-time delivery” → UDP.
Ports and the Four-Tuple
A port (0–65535) identifies a process on a host. A TCP or UDP connection is uniquely identified by the four-tuple: (source IP, source port, dest IP, dest port). This is why thousands of connections can share one server IP — each has a distinct source port. Well-known ports you must know: HTTP 80, HTTPS 443, DNS 53, SSH 22, MySQL 3306, Postgres 5432, Redis 6379. A socket is the OS API object at the endpoint of one such four-tuple.
The TCP Three-Way Handshake
TCP is connection-oriented: before data flows, the endpoints agree on sequence numbers and parameters:
Client Server
│ SYN, seq=1000 │
├──────────────────────────▶ (Server stores client seq)
│ SYN-ACK, seq=5000, ack=1001 │
◀──────────────────────────┤
│ ACK, seq=1001, ack=5001 │
├──────────────────────────▶
│ [ data flows both ways ]
The handshake allocates state on both ends (the SYN-ACK proves the server is reachable and willing). This is also the heart of the classic SYN flood denial-of-service: the server holds half-open connections waiting for the final ACK that never comes.
Teardown uses four packets (FIN → FIN-ACK → FIN → FIN-ACK) — and since teardown is cooperative, a crashed peer leaves “TIME_WAIT” and “CLOSE_WAIT” states that netstat/ss show. A server full of CLOSE_WAIT sockets is a bug in your application (it never closed its end); TIME_WAIT buildup is usually just socket reuse policy.
Reliability: Sequence Numbers and ACKs
Once connected, TCP treats data as a byte stream:
- Every byte has a sequence number (
seq). - The receiver ACKs the next byte it expects (
ack = last received + 1). - The sender retransmits anything not ACKed within a timeout (estimated from round-trip time).
This gives: no loss, no duplication, correct ordering, and an end-to-end checksum. The cost is latency under loss — if a packet drops, the stream stalls until the ACK gap is repaired (in-order delivery = head-of-line blocking). Real-time apps (video calls, games) can’t afford that, which is why they use UDP and accept the loss.
Flow Control vs Congestion Control
Two windows control how much TCP sends, and they answer different questions:
- Flow control — “how much can the receiver take?” The receiver advertises a receive window; the sender never exceeds it (sliding window). Prevents buffer overflow at the destination.
- Congestion control — “how much can the network take?” The sender maintains a congestion window (cwnd) that it probes and shrinks. Prevents overloading routers.
Congestion control is where the magic lives. Modern TCP uses AIMD — Additive Increase, Multiplicative Decrease:
- Slow start — double
cwndeach round trip (exponential) until a threshold or loss. - Congestion avoidance — add ~1 MSS per RTT (linear growth).
- On loss (timeout or triple duplicate ACK) — cut
cwndin half (or reset).
The result is TCP converges on “as much bandwidth as the network safely allows,” oscillating gently around the bottleneck. This is why bulk transfers slow down on lossy links: TCP reads every drop as congestion and backs off — the deep reason “high packet loss = low throughput” even when bandwidth is plenty.
UDP: Best-Effort Datagrams
UDP sends self-contained datagrams with a checksum and ports — no handshake, no state, no retransmission, no ordering:
- Pros: minimal header (8 bytes), zero connection setup, no head-of-line blocking, one-to-many via broadcast/multicast.
- Cons: no delivery guarantee, no ordering, no congestion control — apps must add what they need.
| Uses UDP | Uses TCP |
|---|---|
| DNS (single question/answer) | HTTP/HTTPS, email, file transfer |
| Video/voice (real-time, tolerates loss) | SSH, databases, any reliable stream |
| QUIC (HTTP/3) | Anything needing guaranteed delivery |
| Game/sensor telemetry |
The eternal systems rule: UDP is not “TCP without guarantees” — it’s a different contract. If your application needs reliability on UDP, you rebuild a mini-TCP (sequence numbers, ACKs, retransmission) — which is exactly what QUIC did, with modern congestion control on top of UDP to dodge head-of-line blocking.
TCP in Practice: the Hidden Draggers
Four implementation details explain a mountain of real-world “why is this slow?” mysteries:
- Nagle’s algorithm — batches small writes to reduce tiny segments; interacts badly with interactive protocols unless you disable it (
TCP_NODELAY) for low-latency chat. - Delayed ACK — the receiver waits ~40 ms before ACKing to piggyback on replies; pairs badly with Nagle (the “Nagle + delayed ACK deadlock”).
- Head-of-line blocking — one lost segment stalls every later one, which is why HTTP/3 and QUIC moved to UDP.
- Buffer bloat — long queueing at routers inflates latency; TCP’s only signal for congestion is loss, so it fills buffers (the argument for BBR/AQM schemes).
Practice Trajectory
ss -tn(ornetstat -tn) and identify established connections by their four-tuple; pick one and say which process owns each port.tcpdump -n -c 20 host google.comwhile curling google.com — spot the SYN/SYN-ACK/ACK handshake and the final FIN sequence.curl -w '%{time_connect} %{time_starttransfer}\n'twice — once over TCP/HTTP, and reason about where the handshake and ACK latency go.- Simulate loss with
tc netem loss 5%on a loopback veth and measure throughput drop on a bulk TCP transfer — explain via congestion control. - Explain the four-tuple to someone else from memory, then list three apps that should use UDP and why.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Any reliable client-server data | TCP |
| Real-time media, games, telemetry | UDP |
| Explaining “slow over lossy links” | Congestion control, not bandwidth |
| Debugging connection states | TIME_WAIT/CLOSE_WAIT/half-open are TCP state-machine artifacts |
| Modern HTTP/3 | QUIC = TCP’s guarantees re-built on UDP to kill head-of-line blocking |