How TCP actually talks (and why UDP doesn't bother)
published:
TCP and UDP are the two transport protocols that carry essentially all internet traffic that isn't doing something exotic. They live at the same layer, speak to the same lower-level IP plumbing, and have almost completely opposite personalities.
TCP is a protocol that wants very badly to behave. It sets up connections, numbers every byte, acknowledges what it receives, retransmits what gets lost, slows down when the network complains, and eventually closes the connection neatly. If TCP were a person, it would arrive at meetings early and send a follow-up email.
UDP is a protocol that has politely declined to do any of that. It takes a blob of data, wraps it in a tiny header, and throws it at the network. Whether it gets there is someone else's problem. If UDP were a person, it would be the one who texts "sent" without checking whether you got the message.
Both are the right answer for different things, and the rest of what follows is an attempt to make it clear which is which, and why.
TCP opens with a handshake
Before any data moves, TCP wants to establish that both ends exist, are talking to each other on purpose, and agree on where to start numbering. This takes three packets.
The client sends a SYN with its chosen initial sequence number. The server replies with a SYN-ACK carrying its own sequence number and acknowledging the client's. The client sends one more ACK to confirm. Now both sides know each other is alive, and both sides know a starting byte number, and the connection is officially ESTABLISHED. If you have ever wondered why curl appears to hang for a beat before any bytes come back, this is part of what it's doing.
Notice that no data moves during the handshake. These three packets are pure ceremony. That ceremony is also why TCP has measurable setup latency, which is a big deal if you're connecting to a server on the other side of the planet and your RTT is 300 ms.
Sequence numbers and ACKs keep things honest
Once the connection is open, every byte sent over it has a sequence number. The receiver's job is to keep track of what it's got and tell the sender what it's expecting next. Those are the ACKs. If a segment gets lost, the receiver keeps asking for the byte it never saw, and the sender eventually figures out something is wrong and sends the missing segment again.
Drag the slider through the loss scenarios. In the no-loss case ACKs march back one after another, each asking for the next byte. When a segment goes missing, the receiver keeps asking for the same byte over and over (duplicate ACKs), and the sender retransmits it. Cumulative ACKs mean a single "ack=5" implicitly confirms everything up to byte 4, which is why TCP can survive one lost ACK without restarting the world.
The retransmission is how TCP keeps the "reliable" in "reliable byte stream." From the application's perspective, the bytes either show up in order or the connection dies. There is no "most of the bytes arrived."
A sliding window turns one pipe into many
Sending one segment and then waiting for its ACK is absurdly slow over any network with real distance to cover. TCP instead allows multiple segments to be in flight at once, up to a window size, and only blocks when that window is full.
Bigger window, more packets sitting on the wire at once, higher throughput. Halve the RTT and you halve the time to send a fixed amount of data even without changing the window. Double the window and you roughly double throughput, up to the point where some bottleneck in the network becomes the actual limit. The math here (throughput ≈ window / RTT) is the reason your 1 ms home LAN feels infinitely faster than your 150 ms satellite link, even though both technically support "gigabit."
Congestion control is TCP worrying about bandwidth
TCP has no way of knowing, in advance, how much bandwidth is actually available on any given path. It finds out by experiment. It starts sending slowly, ramps up aggressively until something breaks, retreats, and then ramps up again. This cycle never stops for the life of the connection.
The window grows exponentially during slow start (which despite the name is not slow, it's just starting small) until it reaches the slow-start threshold. After that it creeps up linearly, adding one segment per round trip. Eventually a packet gets dropped, which TCP interprets as the network saying "enough." The window is cut in half, the threshold is moved to the new value, and the cycle starts over. TCP is a protocol that lives its entire life anxious about whether it should be going faster.
This is the source of the famous sawtooth pattern in any graph of TCP throughput over time. It is also why networks with high but variable delay (cellular, satellite, transoceanic) can be fairly miserable for TCP: every burst of delay looks like congestion, the window collapses, and throughput never really gets to stretch out.
UDP: the opposite approach
Everything above exists to provide the illusion of a reliable, ordered byte stream on top of a network that guarantees neither. UDP exists for people who don't want that illusion, because they have a better idea of what to do with the occasional lost packet than the kernel does.
Packets leave the sender. Some of them arrive. The ones that don't arrive are simply gone, and the sender has no idea anything went wrong. There are no ACKs, no retransmits, no sequence numbers that mean anything to the protocol itself, and no back-off. Applications that want any of those things build them on top.
This is wonderful for the applications that benefit from not paying for TCP's services. Real-time voice and video are the classic cases: the receiver would rather glitch for 20 ms than hear a one-second gap while a lost packet gets retransmitted. DNS is another: the query and the response fit in single packets, timeouts are sub-second, and re-querying is cheaper than keeping a connection open. Game state updates are a third: a position that's a quarter-second late is worse than no position at all, because by the time it arrives it's wrong.
When you'd pick which
Reach for TCP when you want bytes to arrive intact and in order, you can tolerate whatever latency that costs, and the application doesn't want to think about packets at all. HTTP, SMTP, SSH, file transfer, databases, most RPC frameworks. The default.
Reach for UDP when late data is worse than lost data, when you care about the cost of a handshake, when you want to multicast to many recipients, or when you have a better reliability scheme than TCP's on top. Real-time media, DNS, NTP, SNMP, most online games, QUIC (which builds its own TCP-replacement on top of UDP specifically because UDP gets out of the way).
Most of the modern protocols people hear about lately (HTTP/3, QUIC, WebRTC) are on UDP. That isn't because TCP has stopped being useful. It's because these protocols want control over exactly the parts of the stack that TCP does not let you touch: connection setup, head-of-line blocking, multiplexing. UDP is the way you get a blank sheet of paper where TCP would otherwise fill in the details for you.
And if you find yourself staring at Wireshark wondering why a perfectly fine TCP connection is suddenly slow, there is a reasonable chance it is not slow at all, just anxiously deciding whether it should be going faster.