TCP & UDP β Part 1: TCP Foundations
What is this?
Picture two people starting a careful phone call. Before either of them launches into the important stuff, they check the line: "Can you hear me?" β "Yes, I can hear you, can you hear me?" β "Great, I can hear you too." Only then does the real conversation begin. And throughout the call, the listener keeps murmuring "mm-hmm... got it... yep" so the speaker knows nothing was lost. If a sentence gets garbled by static, the speaker notices the silence where a "got it" should have been, and repeats it.
That careful, confirmed conversation is TCP. The opening "can you hear me?" exchange is the 3-way handshake. The constant "got it" murmurs are acknowledgements. The repeating of garbled sentences is retransmission. Contrast it with UDP, which is more like shouting a fact across a crowded room: fast, no setup, no confirmation β and if someone didn't catch it, oh well, you've already moved on.
The one-line idea: TCP builds a reliable, ordered stream of bytes on top of an unreliable network β by setting up a connection first, numbering every byte, and re-sending anything that doesn't get acknowledged.
That tension β reliability built on unreliability β is the whole story, and we'll return to it in every part of this series. The network underneath TCP (the IP layer) makes no promises at all: packets can vanish, duplicate, or arrive out of order. TCP is the layer that turns that chaos into something an application can trust.
In the OSI model, TCP lives at Layer 4, the transport layer β it sits directly above IP (Layer 3) and underneath the application protocols you use every day. HTTP/1.1 and HTTP/2 ride on top of TCP. So does TLS/HTTPS, SSH, SMTP, and nearly every database driver. When an L4 load balancer routes "by connection," the connection it's routing is a TCP connection. Get TCP, and a huge amount of the stack suddenly makes sense.
1. Why we even need TCP
The internet's foundation β the IP layer β is gloriously, deliberately unreliable. IP's only job is "try to shove this packet toward that address." It promises nothing about whether the packet arrives, whether it arrives once, or whether it arrives in order. Concretely, on a real network:
- Packets get lost. A router's queue fills up, and the simplest thing it can do is drop the packet on the floor.
- Packets get duplicated. A retransmission timer fires a hair too early and the same data goes out twice.
- Packets arrive out of order. Two packets take different routes; the second one overtakes the first.
- Delay is unpredictable. A packet might cross the country in 30 ms, or get stuck behind a congested link for 300 ms.
No application wants to deal with that directly. Imagine writing a banking app where you had to personally check "did byte 4,012 actually arrive, and was it before or after byte 4,013?" TCP does all of that for you, and hands the application a clean abstraction: a pipe of bytes that come out the other end, in order, complete, exactly once.
Think back to the phone-call analogy, but make it harder β you're dictating a 100-page document over a bad line. Some pages get garbled. Some you say twice. The listener writes them down in whatever order they land. To end up with a correct document, you need three things: a way to number the pages, a way for the listener to confirm which ones arrived, and a way to re-send the missing ones at a pace the listener can keep up with. That is exactly the machinery TCP provides.
2. The four guarantees TCP provides
Everything TCP does collapses into four promises. Hold onto these four words β the rest of the chapter is just how each one is delivered.
- Reliability. Lost data is detected and retransmitted. Nothing the application sent goes missing.
- Ordering. Bytes are delivered to the application in exactly the order they were sent, even if they arrived scrambled.
- Flow control. The sender never overwhelms a slow receiver β the receiver advertises how much buffer space it has, and the sender respects it.
- Congestion control. The sender never overwhelms the network in between β TCP backs off when it senses the path is getting congested.
π‘ Flow control and congestion control sound similar but solve different problems, and confusing them is one of the most common interview stumbles. Flow control protects the receiver (a fast server talking to a slow phone). Congestion control protects the network (everyone sharing a link that's filling up). We'll keep them firmly separated throughout this series.
Together these four make TCP look, to the application above it, like a reliable byte stream. That phrase is worth underlining β it leads directly to the next idea.
3. TCP is a byte stream, not a message protocol
Here's a subtlety that trips up a lot of engineers. TCP does not preserve message boundaries. It hands the application a continuous flow of bytes, like water from a tap:
... byte byte byte byte byte byte byte ...
If your application calls send() three times β say, "Hello", "World", and "!" β TCP is free to deliver all of that as one lump, "HelloWorld!", or chop it into "Hel" and "loWorld!". The bytes are correct and in order, but where one message ends and the next begins is not TCP's concern.
That's why every protocol built on TCP has to define its own framing β its own rule for "where does this message end?" HTTP/1.1 uses a Content-Length header or chunked encoding. HTTP/2 wraps everything in length-prefixed binary frames. TLS has its own record format. They all exist because TCP gives you a stream, not a stack of envelopes.
Contrast this with UDP (Part 2), which does preserve boundaries: one send() becomes exactly one datagram. That single difference β stream vs message β shapes a surprising amount of how each protocol is used.
4. The 3-way handshake: opening the connection
TCP is connection-oriented, which means before any data flows, both sides perform a brief setup ritual to agree they're talking and to synchronize their starting sequence numbers. This is the "can you hear me? β yes β great" of our phone call, and it takes exactly three messages.
Walk through it slowly, because the SEQ/ACK numbers are where the real understanding lives:
- SYN (synchronize). The client picks a random initial sequence number (ISN) β say
100β and sends a segment with theSYNflag set. It's saying: "I want to connect; my byte stream will start numbering at 100." - SYN-ACK. The server replies with both flags. Its
ACKnumber is101β meaning "I received your SYN (which counted as one byte, number 100), so the next byte I expect from you is 101." It also sets its ownSYNwith its own ISN β say300β to start numbering its side of the conversation. - ACK. The client acknowledges the server's ISN:
ack=301. Now both sides know each other's starting numbers, and the connection is ESTABLISHED.
Why three messages and not two? Because the connection is bidirectional, and each direction needs its sequence number both announced (the SYN) and confirmed (the ACK). Two SYNs and two ACKs β but the server piggybacks its SYN onto its ACK, collapsing four logical messages into three packets. That's the "1.5 round trips" of setup cost you'll hear quoted, and it's exactly why connection reuse and pooling matter so much for performance (more in Part 4).
π‘ The random ISN isn't arbitrary β it's a security measure. Predictable sequence numbers would let an attacker inject forged data into your connection. Picking a hard-to-guess starting point makes that attack far harder.
5. What's inside a TCP segment
The fields that make all of this work live in the TCP header, which rides at the front of every segment. You don't need to memorize the bit layout, but you should know what's there and why it matters:
| Field | What it does |
|---|---|
| Source / destination port | Identifies which application on each machine. (The IP address picks the machine; the port picks the process.) |
| Sequence number | The byte offset of this segment's first byte in the stream. The backbone of ordering. |
| Acknowledgement number | "The next byte I expect from you." The backbone of reliability. |
| Flags | SYN, ACK, FIN, RST, PSH, URG β control bits that drive the connection's state machine. |
| Window size | "How much more data I can buffer right now." The mechanism for flow control. |
| Checksum | A simple integrity check so corrupted segments get caught and discarded (then retransmitted). |
The two numbers that do the heavy lifting are sequence and acknowledgement. Every byte in the stream has a number; every ACK tells the sender how far the receiver has gotten. That pair is the engine behind both ordering and reliability, which we'll see next.
6. Reliability: sequence numbers, ACKs, and retransmission
Here is the core loop. The sender numbers its bytes. The receiver acknowledges what it has received contiguously. If an acknowledgement doesn't come back, the sender assumes the data was lost and sends it again. Let's watch a segment go missing.
Two things to notice:
- The receiver ACKs the next byte it wants, not the last it got. When
seq=1100goes missing but1200and1300arrive, the receiver can't acknowledge them β they'd leave a hole. So it keeps re-sendingACK 1100, effectively shouting "I'm still stuck waiting for 1100!" These repeated ACKs are called duplicate ACKs. - The sender has two ways to detect loss. Either a retransmission timer expires with no ACK at all (the slow path, used when ACKs stop entirely), or it receives three duplicate ACKs β a strong hint that one specific segment was lost while later ones got through. The latter triggers fast retransmit: resend the missing segment immediately, without waiting for the timer. We'll dig into the congestion-control side of this in Part 3.
This is where the "ordering" guarantee comes from too: even though 1200 and 1300 arrived before the resent 1100, the receiver holds them in a buffer and only hands the application a clean, in-order stream once the gap is filled. The application never sees the scramble.
7. The sliding window: sending without stalling
If the sender transmitted one segment and then waited for its ACK before sending the next, throughput would be miserable β you'd be limited to one segment per round-trip-time, no matter how fat the pipe. On a link with 100 ms RTT, that's painfully slow.
TCP avoids this with a sliding window: it allows multiple segments to be "in flight" β sent but not yet acknowledged β at once.
The window is the span of bytes the sender may have outstanding without an acknowledgement. As ACKs come back, the left edge advances and the window slides forward, opening room to send more. The bigger the window, the more data in flight, the higher the throughput β roughly, throughput β window size Γ· RTT. On long-distance links, a window too small for the bandwidth-delay product is the single most common cause of disappointing throughput.
So far so good β but who decides how big the window gets? Two different limits, for two different reasons. That's flow control and congestion control.
8. Flow control: don't overwhelm the receiver
The first limit comes from the receiver. Every ACK carries a receive window (rwnd) value: "here's how much free buffer space I have right now." The sender is never allowed to have more unacknowledged data outstanding than the receiver's advertised window. It's the listener on our phone call saying "slow down, let me catch up writing this page."
This matters whenever a fast producer talks to a slow consumer: a beefy backend streaming to a phone on a weak connection, or a high-throughput service feeding a client that processes slowly. Without flow control, the sender would blast data faster than the receiver could drain its buffer, and everything past the buffer would simply be dropped β wasting the network and forcing endless retransmissions. The rwnd=0 case is real: a stalled receiver can tell the sender to fully pause, and the sender periodically sends tiny "window probes" to ask "any room yet?"
Crucially, flow control says nothing about the network in between. A receiver with a huge buffer might advertise a giant window β but if the path between the two can't carry that much, you'd flood the routers and cause loss. Guarding against that is a different job.
9. Congestion control: don't overwhelm the network
The second limit comes from the network itself, and TCP can't see it directly β there's no router that tells the sender "I'm full." So TCP infers congestion from its one available signal: packet loss. The governing assumption is simple and, on wired networks, usually correct:
Lost packet β the network is congested. Slow down.
To act on this, the sender keeps a second, private limit called the congestion window (cwnd) β its own estimate of how much the path can carry. The amount of data actually in flight is bounded by the smaller of rwnd (the receiver's limit) and cwnd (the network's limit). Flow control and congestion control are two separate governors on the same throttle.
New connections don't know the path's capacity, so they start cautiously and ramp up β slow start:
Despite the name, slow start is aggressive at first: cwnd doubles every round trip (1 β 2 β 4 β 8 β 16...), exponentially probing for the ceiling. The moment loss appears, TCP backs off sharply and switches to congestion avoidance, where cwnd grows slowly and linearly (about +1 segment per RTT) β gently testing for more room without overshooting again. This pattern β increase additively, decrease multiplicatively on loss β is called AIMD, and it's what lets thousands of independent TCP flows share a link reasonably fairly. We'll unpack AIMD, fast recovery, and modern algorithms like CUBIC and BBR in Part 3.
The practical sting: slow start means a brand-new connection takes several round trips to reach full speed. For short-lived connections (a single small HTTP request), you might finish before TCP ever ramps up β paying the slow-start tax without ever enjoying the bandwidth. This is a major reason connection reuse, pooling, and protocols that avoid fresh handshakes are such a big deal in real systems.
10. The 4-way close: shutting down cleanly
Just as setup is a handshake, teardown is a handshake too β but it takes four steps, because each direction of the stream is closed independently. Either side can have more to say even after the other is done.
The client sends FIN ("I have no more data"), the server ACKs it, and the client's send-direction is now closed β but the server can keep sending until it's done, at which point it sends its own FIN and gets an ACK back. Four messages, two independent shutdowns.
That final TIME_WAIT state is worth flagging now (we'll revisit it in Part 4): after the last ACK, the closing side lingers for a short while (typically a couple of minutes) before fully releasing the connection. This guards against a stray, delayed packet from the old connection showing up and corrupting a brand-new connection that happens to reuse the same port pair. On busy servers that open and close enormous numbers of short connections, accumulated TIME_WAIT states can pile up and exhaust available ports β a classic production gotcha.
11. The connection's life as a state machine
All of this β the handshake, data transfer, the close β is governed by a state machine every TCP endpoint runs. You don't need to memorize all eleven states, but seeing the shape clarifies how setup and teardown fit together.
The left branch is the active opener and closer; the right branch is the passive side. The two paths through ESTABLISHED and back to CLOSED are the handshake and the 4-way close we just walked through. When you run netstat or ss and see connections sitting in ESTABLISHED, CLOSE_WAIT, or TIME_WAIT, this diagram is what you're looking at β and a pile of CLOSE_WAIT connections usually means an application bug where someone forgot to close their socket.
12. Where TCP shows up β and where it hurts
Because TCP delivers a reliable, ordered stream, it's the default substrate for anything that can't tolerate missing or scrambled data:
- HTTP/1.1 and HTTP/2 β the web's request/response traffic.
- TLS / HTTPS β the encrypted layer the secure web rides on.
- SSH, SMTP, FTP β remote shells, email, file transfer.
- Database drivers β MySQL, PostgreSQL, Redis (in TCP mode), and friends.
But TCP's guarantees come with costs, and recognizing them is what separates a competent engineer from a great one:
- Handshake + slow-start latency punishes short-lived connections. A single tiny request pays for a full setup and ramp-up.
- Head-of-line blocking: because TCP enforces strict ordering, one lost segment stalls delivery of everything behind it β even logically unrelated data. When HTTP/2 multiplexes many requests over one TCP connection, a single lost packet can freeze them all. This is the precise pain point that pushed HTTP/3 onto QUIC (over UDP), which we'll explore in Parts 2 and 3.
- TIME_WAIT accumulation can exhaust ports on connection-churning servers.
π‘ A senior insight worth internalizing: a surprising share of "mysterious latency" in distributed systems comes from TCP behavior β slow start after an idle connection, a retransmission timeout, head-of-line blocking β not from the application logic everyone instinctively blames first.
Key takeaways
- TCP turns an unreliable network into a reliable, ordered byte stream β its four guarantees are reliability, ordering, flow control, and congestion control.
- Connections open with a 3-way handshake (SYN / SYN-ACK / ACK) that synchronizes sequence numbers, and close with a 4-way FIN exchange; the whole lifecycle is a state machine.
- Reliability comes from numbering every byte, ACKing what arrived, and retransmitting what wasn't acknowledged (via timeout or fast retransmit on 3 duplicate ACKs).
- Flow control (receiver's
rwnd) and congestion control (network'scwnd, via slow start and AIMD) are different jobs β one protects the receiver, the other protects the network. Don't conflate them. - TCP's strict ordering causes head-of-line blocking, and its setup costs punish short connections β the very weaknesses that motivated QUIC and HTTP/3 to move onto UDP.
Next up, Part 2 flips the coin: UDP β the shout-across-the-room protocol that throws away TCP's guarantees in exchange for raw speed and total application control.