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.
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.
2 · The first fix
Number every byte, and make the receiver confirm.
3 · The promise
Nothing reaches the app until the run is unbroken.
4 · The price
That promise costs you when the connection is shared.
5 · Keep this card
The whole thing on one index card.
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.
- Head-of-line blocking. If segment 5 is lost, segments 6, 7, 8 might already be in the receiver’s buffer — but the application sees nothing until 5 arrives. For one file, fine. For HTTP/2 multiplexing dozens of independent requests over one TCP connection, one lost packet stalls all of them. This is one structural reason QUIC moved to UDP with per-stream loss recovery.
- Handshake latency. A TCP connection costs one RTT before any application byte flows. Layer TLS on top and you pay another one or two RTTs. On a satellite link or congested mobile, it’s painfully visible. QUIC’s combined transport-and-crypto handshake is largely a response.
- Middleboxes calcified the protocol. NATs, firewalls, and load balancers parse TCP headers, and many grew opinions about what they should look like. New TCP options are sometimes silently dropped or mangled by a hop in the middle, so anything new has to be negotiated defensively and may not survive the path. Evolving TCP on the wire isn’t impossible, just slow and unreliable enough that successors were easier to build on UDP.
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.
Famous related terms
- UDP —
UDP = "send and forget" datagrams over IP— no handshake, no ordering, no retransmits, no flow control. What TCP isn’t. Fine for DNS, voice, games, and anything that prefers “lose it” to “wait for it.” - 3-way handshake —
handshake = SYN → SYN-ACK → ACK— the three messages that exchange and confirm initial sequence numbers before data flows. - TCP congestion control —
congestion control = window that grows on ACKs + shrinks on loss— the mechanism that keeps the shared internet from collapsing when many senders compete. - QUIC —
QUIC = UDP + TLS 1.3 + per-stream reliability + connection IDs— a general-purpose transport built to route around exactly the seams above; it carries HTTP/3, but it isn’t only for browsers. - TLS —
TLS ≈ encryption + identity on top of TCP— what runs above TCP to make connections private and authenticated. - Head-of-line blocking —
HoL = one stall halts the whole stream— the in-order delivery property that becomes a liability under multiplexing.
Going deeper
- RFC 9293 — the current consolidated TCP spec; go here when you want the exact state machine and header semantics rather than a paraphrase of them.
- W. Richard Stevens, TCP/IP Illustrated, Volume 1 — the book to read if you want to watch these mechanisms happen packet by packet in real traces rather than in prose.
- RFC 793 (1981) — read this one for the historical question: what did the designers think they were building, in their own words, before four decades of patches?