Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why → how

What is TCP?

IP delivers packets best-effort — they can vanish, duplicate, or arrive out of order. Almost every program wants a clean stream of bytes instead. TCP is the layer that turns one into the other.

Networking intro Apr 30, 2026 · updated Aug 25, 2026 · 10 min read

On this page

The picture version

Five pictures for a reader who has never thought about how bytes cross a network, following one file: a 5 MB photo sent from a train.

1 · The problem

The layer underneath promises to try. That’s all.

5 MB your photo dropped 12×4 duplicated 1223 reordered 1324 what the photo needs all of the bytes in the order you sent them with no duplicates IP promises to try to deliver. That is the entire promise. TCP is the layer that turns the left-hand column into the right-hand one
Packets get dropped by a congested router, duplicated by a retransmitting link layer, or reordered when two of them take different paths. Almost no application wants to deal with any of that — and a file with silent holes in it is worse than no file.

2 · The first fix

Number every byte, and make the receiver confirm.

every byte gets a number, and the receiver confirms what it has sender keeps a copy receiver stacks by number 337338339340341 photo bytes, numbered “I have everything up to 338” 339 never got confirmed, so it goes again if the retransmit and the original both land, the receiver drops the duplicate — it already holds that number Missing pieces become detectable, and detectable means fixable.
The acknowledgement means “I have everything up to here”, so the sender learns about a gap simply by watching where the ACKs stop advancing. The same numbering that finds the hole also kills the duplicate, because a byte the receiver already has is a byte it can throw away.

3 · The promise

Nothing reaches the app until the run is unbroken.

the receiver does not hand bytes to the application as they land receive buffer 900?901902903904905 nothing released × 900 is missing, so 901–905 wait — even though they are already here In-order delivery is a promise, not a bonus. the buffer sorts by sequence number and releases only a contiguous run which is exactly what makes the photo come out the far end in one piece
Retransmits handle loss; this handles arrival order. Two packets carrying different halves of the photo can take different paths and finish in the wrong sequence, so the receiver buffers, sorts, and releases only what it can release without a gap.

4 · The price

That promise costs you when the connection is shared.

one TCP connection carrying several independent requests request A lost request B request C request D the application sees nothing from any lane B, C and D have nothing to do with A — and their bytes are already in the buffer One lost packet stalls every stream sharing the connection. the structural reason QUIC moved to UDP with per-stream loss recovery
For a single photo this costs nothing — you wanted the whole file anyway. For a browser multiplexing dozens of independent requests over one connection it is the dominant stall. Head-of-line blocking is the price of the word “ordered”, not a bug in it.

5 · Keep this card

The whole thing on one index card.

TCP = best-effort packets + numbering + ACKs + retransmits and a reorder buffer + two windows — the receiver’s, the network’s ∴ a reliable, ordered byte stream the numbering, ACKs and buffer keep that last word true; the windows keep the sender in check
Picture to keep: your photo cut into numbered pages and mailed a batch at a time, with the recipient shouting back “got everything through page 340” — and the sender re-mailing any page that never gets confirmed. Where it breaks: the recipient never sees pages. TCP hands the application an unbroken run of bytes with no message boundaries, which is why protocols on top of it have to mark their own.

Why it exists

You’re on a train, sending a 5 MB photo to a friend. The signal drops in a tunnel, the progress bar stalls, then crawls, then finishes. Your friend opens the photo and it’s perfect — not a smear of corrupted pixels, not the bottom half missing. Nothing about the trip through the tunnel leaked into the file. That gap between a lossy radio link and a byte-perfect photo is the thing worth explaining, and it’s what this post is about. Keep that photo in mind; every mechanism below exists because of something that could happen to it.

The internet’s foundation, IP, makes a deliberately weak promise: I’ll try to deliver this packet to that address. Not that it will arrive. Not that it will arrive once. Not that it will arrive in the order you sent it. Packets can be dropped by a congested router, duplicated by a retransmitting link layer, or reordered when two of them take different paths through the network.

Almost no application wants to deal with that. When you write to a TCP socket, you want to push bytes in one end and have the same bytes — all of them, in order, with no duplicates — come out the other end. That gap, between what IP gives you and what your photo needs, is what TCP (the Transmission Control Protocol) exists to fill.

TCP is old. RFC 793 (1981, Jon Postel ed.) is the original spec; RFC 9293 is the current consolidated one. The core mechanism has survived four decades of hardware change largely intact, which is itself a clue about how well the design holds up.

Why it matters now

Most of the application protocols you deal with still run on top of TCP: web browsing (HTTP and HTTPS), remote logins over SSH, mail delivery and retrieval (SMTP, IMAP), the Postgres, MySQL and Redis wire protocols, and every git push. Plenty of things can make a connection feel slow or hung — DNS, TLS, an overloaded backend, a firewall silently dropping packets — but TCP’s own behavior (retransmit timeouts, a shrunken window, a half-open connection nobody closed) is one of the recurring culprits, and it’s invisible unless you know what the layer promises.

It also matters because it’s what the modern alternatives are reacting against. QUIC only makes sense as a story about which specific frictions of TCP needed routing around.

The short answer

TCP = IP's best-effort packets + numbering + acknowledgements + retransmits + two windows = a reliable, ordered byte stream

Picture to keep: your photo cut into numbered pages and mailed a batch at a time, with the recipient shouting back “got everything through page 340” — and the sender re-mailing any page that never gets confirmed, while the recipient stacks pages in numbered order before reading any of them. The picture breaks in one place worth remembering: the recipient never sees “pages.” TCP hands the application an unbroken run of bytes with no message boundaries, which is why protocols on top of it have to mark their own.

TCP turns IP’s best-effort packet delivery into a byte stream between two endpoints. It opens the connection with a handshake, numbers every byte it sends, has the receiver acknowledge what it got, retransmits what wasn’t acknowledged, reassembles out-of-order arrivals, and adapts its sending rate to both the receiver’s capacity and the network’s.

How it works

Build it up the way it was forced into existence: start with the naive way to send the photo, break it, patch it, break the patch.

Naive attempt: just chop the photo into packets and send them. On a good link this works. In the tunnel it doesn’t — some packets never arrive, and the receiver has no way to even know which ones are missing. A file with silent holes in it is worse than no file.

Fix: number the bytes, and make the receiver confirm. Every byte TCP sends has a sequence number. The receiver sends back ACKs that mean “I have everything up to byte N” (the number on the wire is the next byte expected, which amounts to the same thing). Now missing pieces are detectable: the sender sees which byte the ACKs stopped advancing past. If no ACK arrives within a timeout — and modern TCP measures the path’s RTT and adapts that timeout — it retransmits. If the retransmit and the original both land, the receiver discards the duplicate, because it already has that number.

But numbering only works if both sides agree where the numbers start. Hence the 3-way handshake, before any photo bytes flow. The client sends SYN with its initial sequence number. The server replies SYN-ACK: it acknowledges the client’s number and announces its own. The client replies ACK. Why three and not one? Each side needs to tell the other its starting sequence number and confirm the other received that number. The third message is what makes confirmation symmetric. It also lets the server tell a fresh connection attempt from an old duplicate SYN wandering in late off some slow path — which is the failure a two-message version can’t rule out.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: SYN — "my sequence starts at x"
    S->>C: SYN-ACK — "got x, my sequence starts at y"
    C->>S: ACK — "got y"
    Note over C,S: both sides now know, and confirmed,<br/>each other's starting number — data can flow

Retransmits fix loss, but packets also arrive out of order. Two packets carrying different halves of the photo can take different paths and finish in the wrong order. So the receiver doesn’t hand bytes to the app as they land — it buffers them, sorts by sequence number, and releases only a contiguous run. If byte 1000 arrives before byte 900, it waits in the buffer until 900 shows up. This is what makes the photo come out the far end in one piece.

Now the sender is too eager. With loss handled, nothing stops your phone from pushing bytes faster than your friend’s phone can drain them, and the receive buffer overflows — loss you caused. Fix: every ACK carries a window, “I have room for this many more bytes.” The sender may never have more than that in flight unacknowledged. It’s the receiver’s volume knob on the sender.

And the sender is still too eager — for the network in between. Flow control protects the receiver; nothing yet protects the routers on the path, whose queues fill and start dropping. Fix: a second, sender-side window that grows on clean ACKs and shrinks on loss, reading loss as a hint that some hop is full. When your upload rate sags rather than dying outright, this window is one of the things throttling it — though a radio link has its own reasons to slow down, and telling the two apart from the outside is genuinely hard. It gets its own post — see TCP congestion control for AIMD, CUBIC, BBR, and why it exists at all.

Finally, ending cleanly. Just going silent is ambiguous — the peer can’t distinguish “done” from “fell in a tunnel.” So either side sends FIN (“no more data from me”); the other ACKs, then sends its own FIN, ACKed back. Closes are independent per direction, so half-closed connections are legal: your phone can be done sending the photo while the reply is still coming back.

Show the seams

This is where TCP’s age shows.

You started with TCP = IP packets + numbering + ACKs + retransmits + two windows. What did the chain above add that the one-liner hides? — + in-order delivery is a promise, not a bonus. Numbering, ACKs, retransmits and the reorder buffer all exist to keep that promise — the two windows are doing a different job, keeping the sender from overrunning the receiver and the path — and head-of-line blocking is the price of the promise, not a bug in it. Moving that promise from per-connection to per-stream is one of the things QUIC changed — alongside folding in the crypto handshake, surviving a change of IP address, and escaping the middleboxes above.

Going deeper