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
Use
Reason
DNS
One 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 calls
A retransmitted frame arrives too late to play. Better to drop it and continue — late data is worthless, so reliability is actively harmful.
Gaming
Position updates supersede each other; the newest state is all that matters.
DHCP
You have no IP address yet, so you can't establish a connection.
NTP
Timing precision — a handshake adds delay and jitter.
QUIC / HTTP/3
Wants 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."
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.
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:
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.
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.
State
Means
Many of them means
LISTEN
Waiting for connections
Normal for a server
SYN_SENT
Sent SYN, waiting
Can't reach the server — firewall or wrong port
SYN_RCVD
Half-open
Possible SYN flood
ESTABLISHED
Open
Normal
CLOSE_WAIT
Peer closed, we haven't
Application bug — not calling close()
TIME_WAIT
Waiting out 2×MSL
Normal on the closing side; excessive = port exhaustion risk
FIN_WAIT_2
We closed, peer hasn't
The 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.
AIMD's sawtooth. TCP is permanently probing for more bandwidth and permanently being told no.
The four phases
Phase
Behaviour
Why
Slow start
cwnd 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 avoidance
Past ssthresh, add 1 MSS per RTT (linear)
Near the suspected limit, probe gently rather than doubling into a collapse.
Duplicate ACKs prove packets are still getting through, so the network works — it's mildly congested, not broken. No need to restart.
Timeout
cwnd → 1, back to slow start
Silence 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
Algorithm
Signal
Character
Reno / NewReno
Loss
The classic AIMD sawtooth. Too timid on high-bandwidth long paths.
CUBIC
Loss
Window grows as a cubic function of time since the last loss — fast far from the previous maximum, cautious near it. Linux default.
Vegas
Delay
Reacts to rising RTT before loss occurs. Elegant, but loses badly when competing with loss-based flows — it backs off and they don't.
BBR
Bandwidth + RTT model
Estimates 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
Source
Fix
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-up
Larger initial window; reuse warmed connections
Propagation delay
Only moving the data closer — a CDN or edge PoP
Queueing / bufferbloat
AQM 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.
Four specific problems, none of which could be fixed inside TCP. Naming them is
the whole answer to "why does HTTP/3 exist".
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.
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.
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.
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 on
TCP, in the kernel
UDP, in user space
Handshake
1 RTT TCP + 1 RTT TLS = 2
1 RTT, or 0-RTT resumed
Streams
Multiplexed, but share one ordered byte stream
Independent — loss in one doesn't stall others
Loss recovery
Per connection
Per stream
Encryption
Layered on top; TCP header is plaintext
Integral — almost everything is encrypted, including most of the transport header
Network change
Connection dies
Survives via connection ID
Costs
Mature, hardware-offloaded, universally allowed
Higher 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."