← Computer Networks
Transport Layer

TCP — Handshake, Reliability & Congestion Control

Watch: "TCP Congestion Control" — Gaurav Sen

TCP is the protocol underneath almost every reliable API call, database connection, and HTTP request a backend makes. Interviewers probe it because its mechanisms — reliable delivery, ordering, and congestion control — explain real production symptoms: why a connection stalls, why throughput ramps up slowly on a fresh connection, and why packet loss tanks performance on high-latency links.

What Is TCP?

TCP (Transmission Control Protocol) is the set of rules two computers follow to have a reliable conversation over the internet, even when the network underneath is messy. Before either side sends real data, they shake hands to agree the connection is open; while data flows, TCP numbers every piece so the receiver can put things back in order, notice anything missing, and ask for it again; and it deliberately paces how fast it sends so it doesn't flood the network and make things worse for everyone.

Where This Is Used

A Day-to-Day Example

Think of mailing a multi-page contract to someone in installments. Before sending anything, you'd call ahead to confirm they're ready to receive it and get their address right — that's the handshake. Then you number every page so they can tell if page 4 never arrived and ask you to resend just that page, instead of the whole contract. And if you noticed your last few letters kept coming back "return to sender," you'd naturally slow down and send fewer pages at a time until things cleared up — that's exactly what congestion control does.

Deep Dive: The Three-Way Handshake

TCP three-way handshakeClientServerSYN seq = x"connect? I start at x"SYN-ACK seq = y, ack = x + 1"OK, I start at y; got x"ACK ack = y + 1"got y"connection ESTABLISHED
Three messages, one round trip and a half. Each side picks a starting sequence number and the other side acknowledges it (+1), so both directions are confirmed before any data flows.
client → serverserver → client
Show text version
Client                                  Server
  |------ SYN  (seq=x) ----------------->|   "I want to connect,
  |                                       |    my starting seq number is x"
  |<----- SYN-ACK (seq=y, ack=x+1) ------|   "Got it, I also want to connect,
  |                                       |    my starting seq is y, and I
  |                                       |    acknowledge your x+1"
  |------ ACK  (ack=y+1) ---------------->|   "Confirmed, I received your y+1"
  |                                       |
  |========= connection ESTABLISHED =====|

Both sides need to prove two things: that they can send, and that they received what the other side sent. A two-way handshake (just SYN then SYN-ACK) would let the server think the connection is open, but the client would have no way to confirm the server's sequence number was received before sending data — and the server would have no proof the client actually got the SYN-ACK at all. The third ACK closes that gap: it confirms to the server that the client's receive path works and that both sequence numbers are synchronized before either side commits application data.

Deep Dive: Reliability & Retransmission

Lost segment recovered by fast retransmitSenderReceiverseg 1arrivesack = 2✕seg 2lost in transitseg 3gap: 2 is missingack = 2 (dup 1)seg 4ack = 2 (dup 2)seg 5ack = 2 (dup 3)3rd duplicate ACKseg 2 (fast retransmit)no timer waitack = 63–5 were buffered
The receiver keeps acknowledging the last in-order byte it has (ack = 2), so every later segment produces a duplicate ACK. Three duplicates tell the sender a segment was lost, not just delayed, and it resends at once. If no ACKs come back at all, the retransmission timeout (RTO) fires instead and the segment is resent anyway. (Segment 5 is added here so three duplicates can happen.)
duplicate ACKrecoverylost
Show text version
Sender sends segments 1, 2, 3, 4 (by sequence number)
Segment 2 is lost in transit:

Receiver gets: 1, 3, 4  (gap detected at 2)
Receiver ACKs: ack=2 (still waiting for byte offset 2) — repeated for
               each subsequent out-of-order segment (duplicate ACKs)

Sender sees 3 duplicate ACKs for the same byte offset
  -> triggers FAST RETRANSMIT (resend segment 2 immediately, don't wait
     for the timer)

If no duplicate ACKs arrive at all (total silence):
  -> Retransmission Timeout (RTO) fires, sender resends segment 2 anyway

Every byte in a TCP stream has a sequence number, and the receiver's ACKs reference the next byte offset it expects — this single mechanism gives TCP ordering, duplicate detection, and loss detection simultaneously. Fast retransmit exists because waiting for the RTO timer (which must be conservative to avoid false positives) is slow; three duplicate ACKs are a strong enough signal of loss to retransmit early instead of waiting.

Deep Dive: Flow Control vs Congestion Control

These are the two mechanisms interviewers most often expect you to clearly separate — they solve different problems and use different signals:

FLOW CONTROL                        CONGESTION CONTROL
protects: the RECEIVER               protects: the NETWORK
signal: receiver's advertised        signal: inferred packet loss or
        window (rwnd) — "I have             increasing RTT/ECN marks
        this much buffer left"
mechanism: sliding window            mechanism: congestion window (cwnd),
                                      slow start, AIMD

Sender's actual send rate is bounded by:
   effective window = min(cwnd, rwnd)

A fast sender talking to a slow receiver (e.g. a mobile client with a small buffer) is throttled by flow control even if the network path is empty. A sender on a congested network path is throttled by congestion control even if the receiver has plenty of buffer space. TCP always sends at the rate allowed by the smaller of the two windows — that's the single sentence to give in an interview when asked to distinguish them.

Congestion window over timeinitial ssthresh = 16slow start: ×2 per RTTthen +1 per RTT (AIMD)loss → halvessthresh = 11, +1 againtime (round trips) →cwnd (segments)
The congestion window starts small and doubles every round trip (slow start) until it reaches ssthresh, then grows by one segment per round trip. On a loss the sender halves it and grows linearly again, which produces the classic saw-tooth. (Classic Reno-style behaviour; modern variants like CUBIC and BBR shape the curve differently.)
before lossafter loss
Show text version
Congestion window (cwnd) growth over time:

cwnd
 |                              ___________  <- congestion avoidance
 |                        ___,--'            (linear / AIMD, +1 MSS per RTT)
 |                  __,--'
 |            __,--'  <- slow start (exponential, doubles per RTT)
 |      __,--'
 |  _,-'
 |-'________________________________________ time
      ssthresh reached here, growth slope changes
      (or: loss detected -> cwnd drops sharply, ssthresh updated, repeat)

Deep Dive: Connection Teardown & TIME_WAIT

TCP connection teardown and resetClientServerFIN"I'm done sending"ACKdata (still allowed)half-closedFIN"I'm done too"ACKclient: TIME_WAIT (2 × MSL)Abrupt close, either side:RSTkilled at once
Each direction is closed separately, so a graceful close takes four messages and the connection can be half-open in between. The side that closes first waits in TIME_WAIT so late packets from this connection can't be mistaken for a new one on the same ports.
clientserverreset
Show text version
Client                                  Server
  |------ FIN ---------------------------->|  "I'm done sending"
  |<----- ACK ------------------------------|
  |                                         |  (server may still send more)
  |<----- FIN ------------------------------|  "I'm done too"
  |------ ACK ---------------------------->|
  |                                         |
  [client enters TIME_WAIT for a bounded period before fully closing]

Abrupt close (either side, any time):
  |------ RST ----------------------------->|  connection killed immediately,
                                                no graceful drain, e.g. after
                                                a crash or a refused connection

Each side closes its own half independently (FIN just means "I won't send more," not "stop talking to me entirely") — that's why it takes four steps, not two. TIME_WAIT exists so that the side which sent the final ACK sticks around long enough to guarantee any delayed, duplicate, or retransmitted packets from the now-dead connection don't get misdelivered to a brand-new connection that happens to reuse the exact same 4-tuple. RST is different in kind — it's an immediate, unilateral "this connection is invalid, stop" with no graceful drain, used for crashes, refused connections, or firewall resets.

Key Pointers

How It Works

  1. Client and server complete the SYN / SYN-ACK / ACK handshake, agreeing on initial sequence numbers and options (e.g. window scaling, MSS).
  2. The sender starts with a small congestion window (cwnd) and doubles it each round-trip during slow start.
  3. Once cwnd crosses a threshold (ssthresh) — or loss is detected — TCP switches to congestion avoidance, growing the window by roughly one segment per RTT (additive increase).
  4. On detecting loss (timeout or triple duplicate ACK), TCP cuts cwnd sharply (multiplicative decrease) and re-enters a more conservative growth phase — this is the AIMD (additive-increase/multiplicative-decrease) pattern.
  5. Data continues flowing with sequence numbers and ACKs providing reliability, while cwnd continuously adapts to the network's actual capacity, bounded jointly by the receiver's advertised window.
  6. Connection teardown uses a 4-way FIN/ACK exchange (each side closes its half independently); the side that sends the final ACK enters TIME_WAIT briefly before fully releasing the connection's resources.

Try It Yourself

# Watch the full handshake, data transfer, and teardown live:
sudo tcpdump -i any -nn 'tcp port 443 and host example.com'
# Look for flags: [S] = SYN, [S.] = SYN-ACK, [.] = ACK, [F.] = FIN-ACK, [R] = RST

# Same thing with a friendlier view (if installed):
sudo tshark -i any -f "tcp port 443" -Y "tcp.flags.syn==1 || tcp.flags.fin==1"

# See sockets currently in TIME_WAIT (proof teardown happened recently):
netstat -an | grep TIME_WAIT

# Inspect a live TLS/TCP connection's handshake timing:
curl -v -w "\nconnect: %{time_connect}s  ttfb: %{time_starttransfer}s\n" \
  -o /dev/null -s https://example.com

# Force a look at retransmissions on your machine (Linux):
ss -ti | grep -A1 ESTAB | grep retrans

Interview Q&A

Q: How does TCP guarantee reliability?

A: Through sequence numbers on every byte, cumulative acknowledgments from the receiver, checksums to detect corruption, and retransmission when loss is detected — either via a retransmission timeout or faster fast-retransmit triggered by three duplicate ACKs. If segment 2 of a 4-segment stream is lost, the receiver keeps ACKing "I'm still waiting for byte offset 2" for every subsequent segment it gets, and those duplicate ACKs tell the sender exactly what to resend.

Q: What if an ACK itself is lost?

A: TCP ACKs are cumulative, so this is usually self-healing — if ACK for segment 2 is lost but ACK for segment 3 arrives later, the receiver's "next expected byte" number in that later ACK already covers segment 2 too, since it acknowledges everything received in order up to that point. If no further ACK ever arrives, the sender's retransmission timer eventually fires and it resends the unacknowledged data — worst case, the receiver just gets a harmless duplicate it discards.

Q: What if the network is congested?

A: That's a separate mechanism from packet-level retransmission: congestion control. TCP infers congestion from loss (or explicit congestion notification/ECN marks) and reacts by cutting the congestion window (cwnd) multiplicatively, then ramping back up conservatively through slow start and congestion avoidance rather than immediately resuming full speed. This protects the shared network path itself — it's independent of whether the receiver has spare buffer capacity, which is what flow control handles instead.

Q: Why 3-way handshake and not 2-way?

A: A 2-way handshake only proves the server received the client's SYN — it gives the client no confirmation that its own receive path works, and gives the server no proof the client actually got the server's sequence number. The third ACK closes both gaps: both sides end up with confirmed, synchronized sequence state before any application data is trusted.

Q: FIN vs RST — what's the difference?

A: FIN is a graceful, half-close signal — "I have no more data to send," and the connection drains normally with both sides closing independently via the 4-way exchange. RST is an abrupt, unilateral termination — no drain, no guarantee pending data was delivered — used when a connection is invalid (e.g. connecting to a closed port), after a crash, or when a middlebox/firewall forcibly kills a session.

Q: What is TIME_WAIT and why does it exist?

A: It's the state the side that sent the final ACK of the teardown sits in for a bounded period before fully releasing the connection. It exists to guarantee that any stray, delayed, or duplicated packets from the now-closed connection don't get misinterpreted as belonging to a brand-new connection that happens to reuse the exact same source/destination IP:port tuple soon after.

System Design Tie-in

Slow start is the concrete reason a "design a low-latency API" interview answer should mention connection reuse: a fresh TCP connection needs several round trips before cwnd ramps up to use the link's real capacity, so opening a new connection per request (instead of HTTP keep-alive, connection pooling, or HTTP/2 multiplexing) repeatedly pays that ramp-up cost. Flow control vs congestion control shows up directly when designing a system with mismatched producer/consumer speeds (e.g. streaming a large export to a slow client) — TCP's own flow control already throttles the sender to the receiver's pace, which is why naive "just stream it over a socket" designs can silently self-limit without any application-level backpressure code. TIME_WAIT and port exhaustion show up in any design with a high-churn connection pattern, like a load balancer opening many short-lived backend connections — motivating keep-alive pools to the backend as well as to the client.

Trade-offs

The most common interview trap is conflating flow control and congestion control — flow control protects the receiver's buffer, congestion control protects the network path, and TCP's actual sending rate is bounded by the minimum of both windows. Slow start means a fresh TCP connection is slower to reach full throughput than a warm one, which is exactly why connection reuse/keep-alive and techniques like TCP Fast Open matter for latency-sensitive APIs. High-latency or lossy links (satellite, mobile) punish classic TCP congestion control disproportionately since a single loss event on a high-bandwidth-delay-product path costs many RTTs to recover from — motivating newer algorithms like BBR that model available bandwidth directly instead of reacting only to loss.