CNComputer Networks

Block C · Topics 12–17

Transport

IP delivers packets between machines, unreliably. Transport turns that into something a program can use: which process, and with what guarantees. This is where networking interviews spend most of their time.

12

UDP

connectionlessdatagram8-byte header
Why it exists

Because TCP's guarantees cost round trips, buffering and head-of-line blocking, and for a lot of traffic those costs exceed the benefit. UDP is the minimum viable transport: it adds ports and a checksum to IP and nothing else. Everything TCP does, UDP lets you do yourself — which is a feature when you need different tradeoffs.

What you get, and what you don't

UDP header — 8 bytes total
  ┌──────────────┬──────────────┬──────────────┬──────────────┐
  │  source port │   dest port  │    length    │   checksum   │
  └──────────────┴──────────────┴──────────────┴──────────────┘

Provides:  ports (which process) · a length · an optional checksum
             message BOUNDARIES — one send = one datagram = one receive

Does NOT provide:
  · connection setup      → no handshake, so zero round trips of overhead
  · reliability           → lost datagrams are simply gone
  · ordering              → datagram 2 may arrive before datagram 1
  · flow control          → you can overwhelm a slow receiver
  · congestion control    → you can overwhelm the network
                            (which is why unresponsive UDP flooding is
                             antisocial and gets rate-limited by ISPs)
The framing difference people forget

TCP is a byte stream with no message boundaries: if you send() twice, the receiver may get one combined read or three partial ones. Every TCP protocol therefore needs its own framing — a length prefix, or a delimiter. UDP is message-oriented: one datagram in, the same one datagram out. That's a genuine advantage that gets overlooked because everyone focuses on reliability.

Who chooses UDP, and why

UseReason
DNSOne small query, one small reply. A TCP handshake would triple the packets before you'd even asked. Falls back to TCP for responses over 512 bytes (or with EDNS, larger).
Video / voice callsA retransmitted frame arrives too late to play. Better to drop it and continue — late data is worthless, so reliability is actively harmful.
GamingPosition updates supersede each other; the newest state is all that matters.
DHCPYou have no IP address yet, so you can't establish a connection.
NTPTiming precision — a handshake adds delay and jitter.
QUIC / HTTP/3Wants reliability but implemented in user space, so it can evolve without waiting for OS kernels. UDP is the escape hatch from TCP's ossification.
Metrics, logs (StatsD, syslog)High volume, individual losses irrelevant, and you never want telemetry to block the app.
Say this

"UDP adds ports and a checksum to IP and nothing else — no handshake, no ordering, no retransmission, no congestion control. That's chosen deliberately: for DNS a handshake would triple the packet count for a one-packet question, and for voice a retransmitted audio frame arrives too late to play, so reliability would actively hurt. It also preserves message boundaries, which TCP doesn't — a TCP protocol has to add its own framing. And QUIC is the interesting case: it wants reliability but builds it over UDP in user space, so it can evolve without waiting for every OS kernel to update."

13

TCP connections

three-way handshakefour-way closeTIME_WAITstate machine
Why a handshake at all

Three things have to be agreed before any data can be trusted: that both sides are actually there and willing, the starting sequence numbers in each direction, and options like window scaling and MSS. The handshake is what establishes shared state — and TCP's guarantees are entirely built on both sides agreeing about sequence numbers.

Client Server SYN · seq=x SYN_SENT LISTEN → SYN_RCVD SYN-ACK · seq=y, ack=x+1 ACK · ack=y+1 ESTABLISHED ESTABLISHED data flows both ways — 1 RTT spent FIN FIN_WAIT_1 ACK FIN_WAIT_2 CLOSE_WAIT (app may still send!) FIN LAST_ACK ACK TIME_WAIT (2×MSL, ~60s) CLOSED
Three packets to open, four to close. The asymmetry is because TCP is full duplex — each direction closes independently.
Why open takes 3 and close takes 4

Opening can piggyback: the server's acknowledgement of your SYN and its own SYN travel in one packet, so three suffices. Closing can't be combined in general, because TCP is full duplex and "I have no more data" is a per-direction statement. When the client sends FIN, the server may still have plenty to send — that's the CLOSE_WAIT state, where the connection is half-closed and data still flows one way. Only when the server's application also closes does it send its own FIN.

Practical diagnostic: lots of sockets stuck in CLOSE_WAIT is an application bug — your code isn't calling close() on sockets the peer already closed. The kernel can't fix it for you, and you'll eventually run out of file descriptors.

TIME_WAIT — the state everyone asks about

The side that closes first waits 2×MSL (Maximum Segment Lifetime, so typically ~60 s on Linux) before fully releasing the socket. Two reasons, and knowing both is the good answer:

  1. The final ACK might be lost. If it is, the peer retransmits its FIN and needs someone still there to re-acknowledge it. Releasing immediately would send an RST and leave the peer thinking the close failed.
  2. Old duplicate packets must expire. A delayed segment from this connection could otherwise arrive during a new connection using the same four-tuple and be accepted as valid data. Waiting two maximum segment lifetimes guarantees any straggler is gone.

Why it matters operationally: a busy proxy or load balancer that initiates closures accumulates tens of thousands of TIME_WAIT sockets, exhausting ephemeral ports. The right fixes are connection reuse (keep-alive) and tcp_tw_reuse; the wrong fix is SO_LINGER with zero timeout, which sends an RST and can discard data in flight.

StateMeansMany of them means
LISTENWaiting for connectionsNormal for a server
SYN_SENTSent SYN, waitingCan't reach the server — firewall or wrong port
SYN_RCVDHalf-openPossible SYN flood
ESTABLISHEDOpenNormal
CLOSE_WAITPeer closed, we haven'tApplication bug — not calling close()
TIME_WAITWaiting out 2×MSLNormal on the closing side; excessive = port exhaustion risk
FIN_WAIT_2We closed, peer hasn'tThe peer is leaking connections

Also worth distinguishing: FIN is a graceful close that flushes pending data; RST is an abort that discards it. You get an RST when connecting to a closed port, or from a peer that has already torn the connection down — which is what "connection reset by peer" means.

14

TCP reliability

sequence numberscumulative ACKSACKsliding window
Why it exists

IP loses, duplicates and reorders packets. TCP's job is to present an application with a byte stream that behaves as if none of that happened — using only sequence numbers, acknowledgements and timers.

The mechanisms, in dependency order

  • Sequence numbers count bytes, not packets, and start at a random value (predictable ISNs were a real hijacking vulnerability). They give ordering, duplicate detection, and a way to identify exactly what's missing.
  • Cumulative ACKs. "ACK 5000" means "I have everything up to byte 4999". It's cumulative, so a lost ACK is harmless — the next one covers it.
  • Sliding window. Send up to W unacknowledged bytes before waiting. This is what makes TCP fast: without it you'd send one segment per round trip.
  • Retransmission timeout (RTO), derived from a smoothed RTT estimate plus a variance term, and doubled on each successive failure (Karn's algorithm ignores RTT samples from retransmitted segments, since you can't tell which copy was acknowledged).
  • Fast retransmit. Waiting for a timeout is slow. Three duplicate ACKs for the same sequence number strongly imply one segment was lost while later ones arrived — so retransmit immediately rather than waiting.
Why SACK matters — a favourite follow-up

With plain cumulative ACKs, if you send segments 1–10 and only 3 is lost, the receiver can only keep saying "I have up to 2". The sender doesn't know whether 4–10 arrived, so a naive implementation retransmits all of them — wasting seven segments of bandwidth on a network that just demonstrated it's congested.

Selective acknowledgement adds a TCP option listing the ranges actually received: "I have up to 2, and also 4–10." Now the sender retransmits exactly segment 3. This matters enormously on high-latency or lossy paths, and it's the difference between a mobile connection being usable and unusable.

The TCP header fields worth knowing

source port · dest port          which processes
sequence number                  byte offset of this segment's first byte
acknowledgement number           next byte expected FROM the peer
flags   SYN  open        ACK  acknowledging        FIN  graceful close
        RST  abort       PSH  deliver now          URG  urgent (unused)
window size                      receiver's free buffer — FLOW CONTROL
checksum                         covers header AND payload (unlike IP's)
options                          MSS, window scale, SACK permitted, timestamps

Only 16 bits for the window, i.e. a 64 KB maximum — which was a hard
throughput ceiling on fast long paths until the WINDOW SCALE option
(negotiated in the handshake) allowed multiplying it up to ~1 GB.
See topic 16 for why that mattered so much.

Flow control

The receiver advertises a window size in every ACK: "I have this much buffer space left." The sender never has more than that in flight. If the application stops reading, the window shrinks to zero, the sender stops, and it periodically sends a window probe until the receiver advertises space again.

Two named problems: silly window syndrome, where the receiver advertises tiny windows and the sender ships tiny segments with 40 bytes of header each — fixed by having the receiver wait until a decent chunk is free (Clark's solution) and the sender wait for a full segment (Nagle's). And zero-window deadlock, avoided by the persist timer.

Say this

"Sequence numbers count bytes and start random, ACKs are cumulative, and a sliding window lets multiple segments be in flight so throughput isn't one segment per RTT. Loss is detected two ways: a retransmission timeout, or three duplicate ACKs which trigger fast retransmit without waiting. The refinement worth naming is SACK — with plain cumulative ACKs, losing one segment out of ten leaves the sender unable to tell what arrived, so it may resend all of them; SACK lists the received ranges so only the genuinely missing segment is resent."

15

Congestion control

slow startAIMDfast recoveryCUBICBBR
Why it exists

In October 1986 the link between Berkeley and LBL collapsed from 32 kbps to 40 bps — a thousandfold drop. Senders retransmitted lost packets, which added load, which caused more loss, which caused more retransmission. Congestion control was invented to stop that, and it's the reason the internet is stable at all: there is no central authority regulating traffic, only every endpoint voluntarily backing off.

Flow control vs congestion control — get this right

Flow control protects the receiver from being overwhelmed. It's explicit: the receiver advertises a window. Congestion control protects the network from being overwhelmed. It's implicit: nobody tells you the network is congested, you infer it from loss or delay. The sender's actual limit is min(receive window, congestion window). Mixing these up is one of the most commonly marked-down errors in the subject.

time (round trips) cwnd ssthresh slow start (exponential) congestion avoidance (+1 MSS per RTT) 3 dup ACKs → loss halve cwnd — fast recovery TIMEOUT (worse signal) cwnd → 1, restart slow start The sawtooth: probe upward until loss, back off, repeat — for ever.
AIMD's sawtooth. TCP is permanently probing for more bandwidth and permanently being told no.

The four phases

PhaseBehaviourWhy
Slow startcwnd starts at ~10 segments and doubles every RTT"Slow" is a misnomer — it's exponential. It starts low because a new connection knows nothing about the path, and it climbs fast to find the capacity quickly.
Congestion avoidancePast ssthresh, add 1 MSS per RTT (linear)Near the suspected limit, probe gently rather than doubling into a collapse.
Fast retransmit / recovery3 duplicate ACKs → resend, halve cwnd, continue linearlyDuplicate ACKs prove packets are still getting through, so the network works — it's mildly congested, not broken. No need to restart.
Timeoutcwnd → 1, back to slow startSilence is a much worse signal than duplicate ACKs — possibly a total path failure. Be maximally conservative.

AIMD — additive increase, multiplicative decrease — is the core. It's provably stable and fair: flows converge on an equal share regardless of when they started, because the multiplicative decrease penalises large flows more in absolute terms than small ones.

Modern algorithms

AlgorithmSignalCharacter
Reno / NewRenoLossThe classic AIMD sawtooth. Too timid on high-bandwidth long paths.
CUBICLossWindow grows as a cubic function of time since the last loss — fast far from the previous maximum, cautious near it. Linux default.
VegasDelayReacts to rising RTT before loss occurs. Elegant, but loses badly when competing with loss-based flows — it backs off and they don't.
BBRBandwidth + RTT modelEstimates the actual bottleneck bandwidth and minimum RTT and paces to match, rather than treating loss as the signal. Much better on lossy links (mobile, Wi-Fi) and avoids filling buffers. Google's, and widely deployed.
Bufferbloat, and why loss-based control has a flaw

Loss-based algorithms only slow down when a buffer overflows. Router vendors, trying to avoid loss, fitted very large buffers — so TCP now fills hundreds of milliseconds of buffer before getting any congestion signal. Throughput is fine; latency is ruined, which is why a large download used to make video calls unusable on the same connection. Fixes: smarter queueing at the router (CoDel, FQ-CoDel) that drops early to signal congestion, and delay-aware senders like BBR. This is a great thing to mention — it shows you understand that "no packet loss" is not the same as "good network".

Say this

"Flow control protects the receiver and is explicit — an advertised window. Congestion control protects the network and is implicit — you infer congestion from loss or delay. TCP starts with a small window that doubles each round trip until it hits a threshold, then grows by one segment per RTT. Three duplicate ACKs mean mild congestion, so it halves the window and continues; a timeout means possible path failure, so it drops to one and restarts. The modern nuance is that loss-based control only reacts once buffers overflow, which is why bufferbloat ruins latency, and why BBR models bandwidth and RTT directly instead."

16

Performance & tuning

BDPwindow scalingNaglekeep-alive
Why a fast link can be slow

Because throughput isn't set by bandwidth alone — it's set by how much data you can have in flight, and that's bounded by the window and the round-trip time. This is the single most useful piece of arithmetic in networking.

Bandwidth-Delay Product = bandwidth × RTT
— the amount of data "in the pipe" at any instant, and therefore the window size needed to keep the pipe full.

Max throughput ≈ window size / RTT
Example: 1 Gbps link, 100 ms RTT (London → Singapore)
  BDP = 1,000,000,000 bits/s × 0.1 s = 100,000,000 bits = 12.5 MB

  You need a 12.5 MB window to use that link fully.
  TCP's base window field is 16 bits → 64 KB maximum.

  Throughput with a 64 KB window = 65,536 B / 0.1 s
                                 = 655 KB/s ≈ 5 Mbps
  …on a 1 Gbps link. You are using 0.5% of it.

Fix: the WINDOW SCALE option, negotiated in the handshake, shifts the
window left by up to 14 bits → windows up to ~1 GB.
This is why "long fat networks" needed a protocol extension, and why
window scaling being stripped by a middlebox produces mysteriously
slow-but-working transfers.

Also: a single TCP flow is limited by loss. Parallel connections, or a
protocol like QUIC, work around a single flow's ceiling — which is
partly why download managers and CDNs open several connections.

Latency contributors, and what fixes each

SourceFix
DNS lookup (1 RTT, sometimes more)Caching, prefetch, dns-prefetch
TCP handshake (1 RTT)Connection reuse / keep-alive; TCP Fast Open
TLS handshake (1–2 RTT)TLS 1.3, session resumption, 0-RTT
Slow start ramp-upLarger initial window; reuse warmed connections
Propagation delayOnly moving the data closer — a CDN or edge PoP
Queueing / bufferbloatAQM at the router, BBR at the sender

The reason connection reuse matters so much: a cold HTTPS request costs a DNS lookup, a TCP handshake and a TLS handshake — three or four round trips before the request is even sent. At 150 ms RTT that's half a second of nothing. On a warm connection it's one round trip. This is the entire argument for keep-alive, HTTP/2 multiplexing and connection pooling.

Nagle + delayed ACK — the classic 40 ms stall

Nagle's algorithm (sender side) withholds a small segment until the previous one is acknowledged, to avoid flooding the network with 41-byte packets. Delayed ACK (receiver side) waits up to ~40–200 ms before acknowledging, hoping to piggyback the ACK on a response.

Together they deadlock briefly: the sender waits for an ACK before sending the rest, the receiver waits for more data before ACKing. You get a fixed ~40 ms stall on request-response protocols that write a header and body separately. Fixes: TCP_NODELAY to disable Nagle, or — better — write your whole message in one send(). This is a genuinely common production bug and a great thing to be able to name.

Two more worth knowing: TCP keep-alive (default 2 hours on Linux, far too long for NAT tables that expire in minutes — which is why applications implement their own heartbeats) and the fact that ephemeral port exhaustion caps a single client at ~28,000 concurrent connections to one destination, since the four-tuple must be unique.

17

QUIC

HTTP/30-RTTper-stream recoveryconnection migration
Why replace TCP

Four specific problems, none of which could be fixed inside TCP. Naming them is the whole answer to "why does HTTP/3 exist".

  1. TCP head-of-line blocking. HTTP/2 multiplexes many streams over one TCP connection — but TCP delivers a single ordered byte stream, so one lost packet stalls every stream until it's retransmitted. HTTP/2 solved application-layer blocking and left transport-layer blocking untouched.
  2. Handshake cost. TCP's 1 RTT plus TLS's 1–2 RTT means 2–3 round trips before any data. QUIC merges transport and cryptographic handshakes into one, and offers 0-RTT for a resumed connection.
  3. Ossification. TCP lives in the kernel and is inspected by middleboxes that reject anything unfamiliar, so changes take a decade to deploy. QUIC runs in user space over UDP and is fully encrypted, so it can be updated with a browser release.
  4. Connection death on network change. A TCP connection is identified by its four-tuple, so switching from Wi-Fi to mobile changes your IP and kills every connection. QUIC uses a connection ID independent of the addresses, so it survives the switch.
TCP + TLS 1.3 (HTTP/2)QUIC (HTTP/3)
Runs onTCP, in the kernelUDP, in user space
Handshake1 RTT TCP + 1 RTT TLS = 21 RTT, or 0-RTT resumed
StreamsMultiplexed, but share one ordered byte streamIndependent — loss in one doesn't stall others
Loss recoveryPer connectionPer stream
EncryptionLayered on top; TCP header is plaintextIntegral — almost everything is encrypted, including most of the transport header
Network changeConnection diesSurvives via connection ID
CostsMature, hardware-offloaded, universally allowedHigher CPU (user-space crypto, less offload); some networks block or throttle UDP; harder to debug and to inspect for security tooling
Be careful with 0-RTT

0-RTT data is sent before the handshake completes, using keys from a previous session — so it is replayable: an attacker who captures it can resend it. It's therefore only safe for idempotent requests. A GET is fine; a POST that transfers money is not. Naming this limitation unprompted is a strong signal.

Say this

"HTTP/2 multiplexes streams over one TCP connection, but TCP still delivers one ordered byte stream — so a single lost packet stalls every stream. That's transport head-of-line blocking, and you can't fix it inside TCP. QUIC rebuilds reliability over UDP in user space with independent per-stream recovery, merges the transport and TLS handshakes into one round trip, and identifies connections by an ID rather than the IP four-tuple so they survive switching from Wi-Fi to mobile. The costs are more CPU, less hardware offload, and some networks treating UDP with suspicion."