Introduction to TCP
TCP vs UDP comparison already laid out the trade: TCP trades setup latency and a bigger header for guaranteed, ordered, congestion-aware delivery. That article showed the header format and the three-way handshake in a single packet capture, enough to recognize what TCP looks like on the wire. This sub-module opens that box properly — the handshake, the teardown, the sliding window, and the two forms of self-restraint (flow control and congestion control) that make a "reliable" protocol behave well on a network it doesn't own.
Why TCP needs all of this machinery
Every guarantee TCP makes has a mechanism behind it, and every mechanism costs something. Reliable delivery needs a way to detect loss, which means numbering everything and tracking what's been confirmed — that's what the next two articles, Three-way handshake and Connection teardown, set up and tear down. Sending more than one unacknowledged segment at a time without waiting on every reply needs a bookkeeping scheme — that's the Sliding window. Not overwhelming a slow receiver needs the receiver to say how much it can hold — Flow control. Not overwhelming the network itself, which is a shared resource TCP can't see directly, needs an entirely separate discipline — Congestion control, and the family of algorithms that implement it: Slow start, TCP Reno, TCP CUBIC, and TCP BBR. And because none of this happens in a vacuum — it all rides inside IP packets with a size limit — the module closes with MTU, MSS, and Fragmentation, the constraints that decide how big a single TCP segment is allowed to be in the first place.
The two problems TCP solves that look like one
It's easy to lump "flow control" and "congestion control" together as "TCP being careful," but they answer different questions and exist because of different limits:
- Flow control asks: how much can the receiving application handle? A receiver has a finite buffer. If the sender pushes data faster than the receiving process reads it out of that buffer, the buffer fills and the receiver has no choice but to drop what it can't hold — a slow database client falling behind a fast server is the classic case.
- Congestion control asks: how much can the network path in between handle? A router or switch between sender and receiver has its own finite buffer, shared across every other connection passing through it at the same time. TCP doesn't get a live status report from that hardware; it has to infer congestion from what it can observe — mainly loss and delay — and react.
Both limits are real, both are expressed as a window — a cap on how much unacknowledged data may be in flight — and the sender actually uses the smaller of its congestion window and the receiver's advertised window at any given moment. But conflating them is a common source of confusion later in this module, so it's worth fixing the distinction now: one is about the receiver's memory, the other is about the network's capacity, and TCP tracks them as two entirely separate numbers.
What a TCP connection actually costs
Nothing in the list above is free, and the cost shows up as latency before the first useful byte ever moves. The handshake spends one full round trip before any application data can be sent. Once data starts flowing, slow start deliberately ramps up gradually rather than sending at full capacity from the first segment — a brand-new connection is, by design, not yet trusted with a large amount of unacknowledged data. On a connection with a high round-trip time — a mobile client on another continent from the server, say — that ramp-up period is where a real share of a request's total latency actually goes, well before server-side processing time enters the picture at all.
This is exactly why long-lived, reused TCP connections outperform a fresh connection per request even when the total bytes transferred are identical: a connection that's already survived several round trips has usually grown its congestion window past the cautious opening values, so it can send more data per round trip without a fresh handshake tax on top. A reverse proxy or a database connection pool that keeps warm, already-established connections to backend servers isn't just avoiding handshake overhead — it's avoiding paying the slow-start tax over and over on every new request. That single design fact, more than any individual command in this module, is why connection reuse shows up so often as the fix for a "slow API" investigation.
Reading a connection's state
A TCP connection isn't just "open" or "closed." Both ends maintain an explicit state — the operating system tracks it per socket — and that state changes predictably as the handshake and teardown proceed. ss shows it directly:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
ESTAB 0 0 10.0.5.12:51900 93.184.216.34:443
TIME-WAIT 0 0 10.0.5.12:51884 93.184.216.34:443
CLOSE-WAIT 0 312 10.0.5.12:51872 93.184.216.34:443
Four different states, four different meanings: a socket waiting for incoming connections (LISTEN), one with an active, fully negotiated conversation (ESTAB), one that has finished but is holding its slot open to catch any stray late-arriving segments (TIME-WAIT), and one where the remote side has already closed but the local application hasn't called close() on its end yet (CLOSE-WAIT) — often a sign of a resource or connection leak worth investigating on its own. Connection teardown walks through exactly how a connection reaches each of these states and why TIME-WAIT in particular exists rather than being an oversight.
Where to start
The rest of this module builds in one direction: from establishing a connection, through moving data safely across it, to accounting for the fact that every one of TCP's segments still has to physically fit inside an Ethernet frame. Start with Three-way handshake (SYN → SYN-ACK → ACK) — it's the step that turns two independent hosts into what TCP treats as a single, stateful conversation, and every mechanism after it depends on that state already existing.