TCP
Reliable Communication
LinkedIn Hook
The secret every senior engineer knows about the internet:
Every web request you make has an invisible negotiation before a single byte of your actual data is sent.
Your browser and the server shake hands — three times — before they exchange anything useful. And when data is lost in transit, TCP quietly re-fetches it without you ever knowing.
Here is what most developers do not fully understand about TCP:
- The 3-way handshake (SYN → SYN-ACK → ACK) is why there is always a small latency cost to the first request on a new connection
- Sequence numbers are how TCP guarantees data arrives in the correct order — even if packets take different physical paths
- Flow control (sliding window) prevents a fast sender from overwhelming a slow receiver — without any application code on your end
- Congestion control (slow start, AIMD) is why your download doesn't flood the network — TCP is self-throttling by design
Every HTTP request, every database query, every SSH session runs on TCP. Understanding it makes you better at debugging latency, tuning connection pools, and designing backend systems.
Full lesson → [link]
#Networking #TCP #BackendEngineering #SystemDesign #InterviewPrep
What You'll Learn
- What TCP is and why it is called "connection-oriented"
- The 3-way handshake — step by step — and what SYN, SYN-ACK, and ACK mean
- How TCP guarantees ordered, reliable delivery using sequence numbers and acknowledgements
- Flow control (sliding window) and congestion control (slow start) — what they are and why they matter
- The 4-way teardown (FIN handshake) and the TIME_WAIT state
- When TCP is used in practice and its performance tradeoffs
The Analogy That Makes This Click
TCP is a phone call. Before either person says anything useful, there is a ritual:
- You dial. The phone rings. (SYN — "I want to connect")
- The other person picks up. (SYN-ACK — "I hear you, I am ready")
- You say "Hello?" and hear the response. (ACK — "Great, connection confirmed")
Now you are both committed to the conversation. If one of you says something garbled, the other asks "Sorry, could you repeat that?" (retransmission). You both agree on the order of sentences — you do not respond to something you have not heard yet. When you are done, there is another ritual: "Goodbye." "Goodbye." (FIN, FIN-ACK).
This is the price of reliability: setup overhead. The tradeoff is that once the call is connected, you can say anything and trust it will be heard correctly.
What Is TCP?
TCP (Transmission Control Protocol) is a Layer 4 (Transport Layer) protocol. It provides:
- Connection-oriented communication — a connection must be explicitly established before data transfer
- Reliable delivery — every segment sent is acknowledged; unacknowledged segments are retransmitted
- Ordered delivery — data arrives in the exact order it was sent, regardless of the path packets took
- Flow control — the receiver tells the sender how much data it can handle; the sender does not overwhelm it
- Congestion control — TCP detects network congestion and slows its send rate to avoid making things worse
- Error detection — a checksum on every segment catches corruption in transit
TCP achieves all of this transparently — applications using TCP do not implement any of these mechanisms themselves.
The 3-Way Handshake
Before any data is exchanged, TCP establishes a connection through a 3-way handshake. This synchronizes both sides on the sequence numbers they will use to track data.
Client Server
| |
|-------- SYN (seq=1000) ------>| Step 1: Client wants to connect
| | seq=1000 is client's ISN
|<--- SYN-ACK (seq=5000, | Step 2: Server acknowledges and
| ack=1001) --------| responds with its own ISN
| | ack=1001 means "got seq 1000,
| | send me seq 1001 next"
|-------- ACK (ack=5001) ------>| Step 3: Client acknowledges server's ISN
| |
| === Connection Established ===
| |
|====== DATA TRANSFER ==========|
SYN — Synchronize. The client picks a random Initial Sequence Number (ISN) and sends it. This is not arbitrary — the ISN is randomized to prevent TCP sequence prediction attacks (an old security exploit).
SYN-ACK — The server acknowledges the client's ISN (ACK = client ISN + 1) and sends its own ISN. The +1 means "I have received everything up to and including your ISN, now send me the next byte."
ACK — The client acknowledges the server's ISN. Both sides are now synchronized. The connection is open.
The handshake adds one full round-trip time (RTT) of latency before data can flow. This is why HTTP keep-alive and HTTP/2 multiplexing exist — to reuse connections and avoid paying this overhead repeatedly.
Sequence Numbers & Acknowledgements
After the handshake, every byte sent by TCP has a sequence number. The receiver sends back an acknowledgement number (ACK) indicating the next byte it expects.
Client sends: segment with seq=1001, data=500 bytes
→ carries bytes 1001 through 1500
Server replies: ACK ack=1501
→ "I got everything up to 1500, send me 1501 next"
Client sends: segment with seq=1501, data=500 bytes
Server replies: ACK ack=2001
If a segment is lost (no ACK received within a timeout), the sender retransmits it. If segments arrive out of order, TCP buffers them and delivers them to the application in order.
Cumulative ACK: A single ACK can acknowledge multiple segments. If segments 1, 2, and 3 all arrive, the receiver sends one ACK saying "I have received through segment 3."
Flow Control — Sliding Window
The receiver controls how much data the sender can have "in flight" (sent but not yet acknowledged) at any moment. This is the receive window (rwnd), advertised in every ACK.
Sender can have at most: rwnd bytes unacknowledged at any time
If rwnd = 64,000 bytes:
Sender sends up to 64KB before waiting for an ACK
If receiver's buffer is filling up:
rwnd shrinks → sender slows down
If receiver's buffer empties (app reads data):
rwnd grows → sender speeds up
This prevents a fast server from sending data faster than a slow client (e.g., a mobile device with limited buffer space) can process it.
Congestion Control — Slow Start & AIMD
Flow control protects the receiver. Congestion control protects the network — it prevents any single TCP connection from sending so fast that it causes router queues to fill and packets to be dropped everywhere.
TCP uses a congestion window (cwnd) — the sender's own self-imposed limit based on perceived network conditions.
Slow Start
When a new connection opens, TCP does not know the network's capacity. It starts conservatively:
Round 1: cwnd = 1 segment (send 1)
Round 2: cwnd = 2 segments (send 2 — on getting ACK for round 1)
Round 3: cwnd = 4 segments (exponential growth)
Round 4: cwnd = 8 segments
...
This doubles each RTT until it hits the slow start threshold (ssthresh) — then switches to Additive Increase:
After ssthresh: cwnd grows by 1 segment per RTT (linear growth)
Additive Increase / Multiplicative Decrease (AIMD)
When TCP detects packet loss (a timeout or 3 duplicate ACKs — which signal congestion):
ssthresh = cwnd / 2 (cut threshold in half)
cwnd = 1 or cwnd/2 (depending on algorithm variant — Reno, CUBIC, BBR)
This "probe and back off" behavior is why TCP is considered fair — all connections competing on a bottleneck link will settle at roughly equal shares.
The 4-Way Teardown
Closing a TCP connection requires 4 steps — one side initiates, but both sides must close their half independently:
Client Server
| |
|-------- FIN (seq=X) --------->| Client: "I have no more data to send"
|<------- ACK (ack=X+1) --------| Server: "Got it"
| | (Server may still send data)
|<------- FIN (seq=Y) ----------| Server: "I have no more data either"
|-------- ACK (ack=Y+1) ------->| Client: "Got it"
| |
| === Connection Closed ===
After the client sends the final ACK, it enters TIME_WAIT — it waits for 2×MSL (Maximum Segment Lifetime, typically 60–120 seconds) before fully closing. This ensures any delayed packets from the old connection do not corrupt a new connection on the same port.
TIME_WAIT is why high-traffic servers run out of ephemeral ports — thousands of sockets stuck in TIME_WAIT can exhaust the port range if connections are opened and closed rapidly.
Code Example 1 — TCP Connection Simulation
// TCP connection lifecycle simulation — handshake, data transfer, teardown
// Models sequence numbers and ACKs to show how reliability works
class TCPEndpoint {
constructor(name, initialSeq) {
this.name = name;
this.seq = initialSeq; // current sequence number (next byte to send)
this.ack = 0; // next sequence number expected from peer
this.state = "CLOSED";
this.buffer = []; // received but undelivered data
}
sendSYN() {
this.state = "SYN_SENT";
const seg = { type: "SYN", seq: this.seq, ack: 0 };
console.log(`[${this.name}] → SYN seq=${this.seq} state: ${this.state}`);
return seg;
}
receiveSYN(seg) {
this.ack = seg.seq + 1; // next expected: ISN + 1
this.state = "SYN_RECEIVED";
const reply = { type: "SYN-ACK", seq: this.seq, ack: this.ack };
console.log(`[${this.name}] ← SYN seq=${seg.seq} | → SYN-ACK seq=${this.seq} ack=${this.ack} state: ${this.state}`);
this.seq++; // SYN consumes one sequence number
return reply;
}
receiveSYNACK(seg) {
this.ack = seg.seq + 1;
this.state = "ESTABLISHED";
const reply = { type: "ACK", seq: this.seq, ack: this.ack };
console.log(`[${this.name}] ← SYN-ACK seq=${seg.seq} | → ACK ack=${this.ack} state: ${this.state}`);
return reply;
}
receiveACK(seg) {
this.state = "ESTABLISHED";
console.log(`[${this.name}] ← ACK ack=${seg.ack} state: ${this.state}`);
}
sendData(data) {
const seg = { type: "DATA", seq: this.seq, data, length: data.length };
console.log(`[${this.name}] → DATA seq=${this.seq} "${data}" (${data.length} bytes)`);
this.seq += data.length;
return seg;
}
receiveData(seg) {
if (seg.seq !== this.ack) {
console.log(`[${this.name}] !! Out-of-order segment seq=${seg.seq}, expected ${this.ack} — buffering`);
this.buffer.push(seg);
return null;
}
this.ack = seg.seq + seg.length;
const reply = { type: "ACK", seq: this.seq, ack: this.ack };
console.log(`[${this.name}] ← DATA seq=${seg.seq} "${seg.data}" | → ACK ack=${this.ack}`);
return reply;
}
sendFIN() {
this.state = "FIN_WAIT";
const seg = { type: "FIN", seq: this.seq };
console.log(`[${this.name}] → FIN seq=${this.seq} state: ${this.state}`);
this.seq++;
return seg;
}
receiveFIN(seg) {
this.ack = seg.seq + 1;
this.state = "CLOSE_WAIT";
const reply = { type: "ACK", seq: this.seq, ack: this.ack };
console.log(`[${this.name}] ← FIN seq=${seg.seq} | → ACK ack=${this.ack} state: ${this.state}`);
return reply;
}
}
// === Simulation ===
const client = new TCPEndpoint("Client", 1000);
const server = new TCPEndpoint("Server", 5000);
console.log("\n=== 3-WAY HANDSHAKE ===");
const synSeg = client.sendSYN();
const synAckSeg = server.receiveSYN(synSeg);
const ackSeg = client.receiveSYNACK(synAckSeg);
server.receiveACK(ackSeg);
console.log("\n=== DATA TRANSFER ===");
const req = client.sendData("GET / HTTP/1.1");
const ack1 = server.receiveData(req);
client.receiveACK(ack1);
const res = server.sendData("HTTP/1.1 200 OK");
const ack2 = client.receiveData(res);
server.receiveACK(ack2);
console.log("\n=== 4-WAY TEARDOWN ===");
const fin1 = client.sendFIN();
const ack3 = server.receiveFIN(fin1);
client.receiveACK(ack3);
const fin2 = server.sendFIN();
const ack4 = client.receiveFIN(fin2);
server.receiveACK(ack4);
console.log("\n[Client] Entering TIME_WAIT (waiting 2×MSL before fully closing)");
Code Example 2 — TCP State Machine
// TCP connection state machine
// Shows all valid TCP states and transitions
const TCP_STATES = {
CLOSED: "CLOSED",
LISTEN: "LISTEN",
SYN_SENT: "SYN_SENT",
SYN_RECEIVED: "SYN_RECEIVED",
ESTABLISHED: "ESTABLISHED",
FIN_WAIT_1: "FIN_WAIT_1",
FIN_WAIT_2: "FIN_WAIT_2",
TIME_WAIT: "TIME_WAIT",
CLOSE_WAIT: "CLOSE_WAIT",
LAST_ACK: "LAST_ACK",
};
// Transitions: [currentState, event] -> nextState
const TRANSITIONS = [
// Server side (passive open)
["CLOSED", "passive_open", "LISTEN"],
["LISTEN", "recv_SYN", "SYN_RECEIVED"],
["SYN_RECEIVED", "send_SYN_ACK", "SYN_RECEIVED"],
["SYN_RECEIVED", "recv_ACK", "ESTABLISHED"],
// Client side (active open)
["CLOSED", "active_open", "SYN_SENT"],
["SYN_SENT", "recv_SYN_ACK", "ESTABLISHED"],
// Data transfer
["ESTABLISHED", "data", "ESTABLISHED"],
// Active close (initiator)
["ESTABLISHED", "send_FIN", "FIN_WAIT_1"],
["FIN_WAIT_1", "recv_ACK", "FIN_WAIT_2"],
["FIN_WAIT_2", "recv_FIN", "TIME_WAIT"],
["TIME_WAIT", "timeout_2MSL", "CLOSED"],
// Passive close (receiver)
["ESTABLISHED", "recv_FIN", "CLOSE_WAIT"],
["CLOSE_WAIT", "send_FIN", "LAST_ACK"],
["LAST_ACK", "recv_ACK", "CLOSED"],
];
const transitionMap = new Map(
TRANSITIONS.map(([from, event, to]) => [`${from}:${event}`, to])
);
function simulate(startState, events) {
let state = startState;
console.log(` State: ${state}`);
for (const event of events) {
const key = `${state}:${event}`;
const next = transitionMap.get(key);
if (!next) {
console.log(` [${event}] → INVALID transition from ${state}`);
break;
}
state = next;
console.log(` [${event}] → ${state}`);
}
}
console.log("\n=== Client (active open) ===");
simulate("CLOSED", [
"active_open", "recv_SYN_ACK", "data", "data", "send_FIN", "recv_ACK", "recv_FIN", "timeout_2MSL"
]);
console.log("\n=== Server (passive open) ===");
simulate("CLOSED", [
"passive_open", "recv_SYN", "send_SYN_ACK", "recv_ACK", "data", "recv_FIN", "send_FIN", "recv_ACK"
]);
Common Mistakes
Mistake 1 — Thinking the 3-way handshake has zero cost
Each network round-trip takes time. An HTTP request to a server 100ms away costs at least 100ms just for the SYN, another 100ms for the SYN-ACK, and only then can data be sent. This is why HTTP keep-alive (reusing the same TCP connection for multiple requests) and HTTP/2 (multiplexing multiple requests on one connection) are critical performance wins. Cold TCP connections are slow. Warm ones are not.
Mistake 2 — Confusing flow control with congestion control
Flow control (rwnd) is the receiver telling the sender "slow down, I cannot process data that fast." Congestion control (cwnd) is the sender deciding on its own "the network seems congested, I will slow down." Both controls set a limit on in-flight data; TCP uses min(rwnd, cwnd) as the effective window. They solve different problems: one protects the receiver, the other protects the shared network.
Mistake 3 — Ignoring TIME_WAIT in high-connection-rate services
TIME_WAIT is not a bug — it prevents old packets from corrupting new connections. But if your server creates and closes thousands of connections per second (e.g., short-lived REST calls without connection pooling), you can exhaust ephemeral ports. The fix is connection pooling (reuse TCP connections) or configuring SO_REUSEADDR / TCP_TW_REUSE on the socket — not disabling TIME_WAIT entirely.
Interview Questions
Q: What is the TCP 3-way handshake and why is it needed?
The 3-way handshake establishes a TCP connection before any data is transferred. Step 1: the client sends a SYN with its Initial Sequence Number (ISN). Step 2: the server acknowledges the client's ISN (ACK = ISN + 1) and sends its own ISN in a SYN-ACK. Step 3: the client acknowledges the server's ISN. After this exchange, both sides have agreed on the sequence numbers they will use to track data in each direction. It is needed because TCP is full-duplex — each direction of communication must independently synchronize sequence numbers.
Q: What is TIME_WAIT and why does it exist?
After the active-close side sends the final ACK, it enters TIME_WAIT for 2×MSL (usually 60–120 seconds) before releasing the port. It exists to handle two cases: (1) the final ACK might be lost — if the server retransmits its FIN, the client needs to still be alive to re-send the ACK; (2) old duplicate packets from the now-closed connection might still be in the network — TIME_WAIT ensures they expire before a new connection can reuse the same 4-tuple (src IP, src port, dst IP, dst port).
Q: What is the difference between TCP flow control and congestion control?
Q: How does TCP guarantee ordered delivery?
Every byte in a TCP stream has a sequence number. The receiver reassembles segments in order regardless of arrival sequence. If segment 2 arrives before segment 1, it is buffered until segment 1 arrives. The receiver's ACK always specifies the next byte it expects — so the sender knows exactly what has been received and what needs retransmission.
Q: Why is TCP described as "full-duplex"?
TCP maintains independent sequence numbers and acknowledgements for each direction — client→server and server→client are tracked separately. Both sides can send data simultaneously without either direction blocking the other. The 3-way handshake synchronizes sequence numbers for both directions, which is why it takes 3 steps (not 2 — a 2-way handshake cannot synchronize both directions simultaneously).
Quick Reference — Cheat Sheet
TCP KEY PROPERTIES
─────────────────────────────────────────────
Connection : Required (3-way handshake first)
Reliability : Guaranteed — retransmits lost segments
Ordering : Guaranteed — sequence numbers reorder if needed
Speed : Slower (overhead of ACKs, handshake, flow/congestion control)
Use cases : HTTP/HTTPS, SSH, database queries, email, file transfer
3-WAY HANDSHAKE
─────────────────────────────────────────────
Client → Server SYN (seq=ISN_c)
Server → Client SYN-ACK (seq=ISN_s, ack=ISN_c+1)
Client → Server ACK (ack=ISN_s+1)
Cost: 1 RTT before any data can be sent
4-WAY TEARDOWN
─────────────────────────────────────────────
Active → Passive FIN
Passive → Active ACK
Passive → Active FIN
Active → Passive ACK → TIME_WAIT (2×MSL)
KEY CONTROLS
─────────────────────────────────────────────
rwnd (receive window) : Receiver controls how much sender can send
cwnd (congestion window): Sender self-limits based on network feedback
Effective window : min(rwnd, cwnd)
Slow start : cwnd doubles per RTT until ssthresh
TCP HEADER FIELDS (20 bytes minimum)
─────────────────────────────────────────────
Source port 2 bytes
Dest port 2 bytes
Sequence number 4 bytes
ACK number 4 bytes
Flags 1 byte (SYN, ACK, FIN, RST, PSH, URG)
Window size 2 bytes (rwnd)
Checksum 2 bytes
Urgent pointer 2 bytes
Previous: Lesson 3.4 — Subnetting & CIDR → Next: Lesson 4.2 — UDP — Fast Communication →
This is Lesson 4.1 of the Networking Interview Prep Course — 8 chapters, 32 lessons.