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.
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.
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.
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.
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 anywayEvery 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.
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.
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.)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)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 connectionEach 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.
SYN, server responds SYN-ACK, client responds ACK — both sides exchange and acknowledge initial sequence numbers before any data flows.rwnd).min(cwnd, rwnd).ssthresh or loss), congestion avoidance (linear/AIMD growth after that), and multiplicative decrease on detected loss.cwnd) and doubles it each round-trip during slow start.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).cwnd sharply (multiplicative decrease) and re-enters a more conservative growth phase — this is the AIMD (additive-increase/multiplicative-decrease) pattern.cwnd continuously adapts to the network's actual capacity, bounded jointly by the receiver's advertised window.# 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
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.
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.
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.