ismile@portfolio:~$ cat notes/0012-tcp-deep-dive.md
TCP Deep Dive
TCP communication has 3 phases:
- Connection Establishment — 3-way handshake
- Data Transfer — segmented payloads with sequence numbers, ACKs, checksums, loss/recovery, and flow control
- Connection Termination — 4-step FIN/ACK
Rather than jumping between unrelated examples, this walkthrough follows one TCP connection from open to close. The client sends the string "hello hello hello hello hello " (30 bytes), split into 5 segments of 6 bytes each. Every sequence number, ACK number, and checksum below belongs to this same connection, so you can trace a single byte stream through every phase.
Phase 1: Connection Establishment (3-Way Handshake)
Client Server
| |
| SYN, seq=1000 |
| ------------------------------>
| |
| SYN, ACK, seq=2000, |
| ack=1001 |
| <------------------------------
| |
| ACK, seq=1001, ack=2001 |
| ------------------------------>
Segment 1 — SYN
Source Port: 54321
Destination Port: 8080
Sequence Number: 1000
Acknowledgment Number: 0
Flags: SYN
Window Size: 65535
Checksum: 0x7A31
Payload: (none)
"I want to start a connection. My first byte will be numbered 1000."
Segment 2 — SYN-ACK
Source Port: 8080
Destination Port: 54321
Sequence Number: 2000
Acknowledgment Number: 1001
Flags: SYN, ACK
Window Size: 65535
Checksum: 0xC108
Payload: (none)
"Agreed. My first byte is 2000. I acknowledge your SYN — next byte I expect from you is 1001."
Segment 3 — ACK
Source Port: 54321
Destination Port: 8080
Sequence Number: 1001
Acknowledgment Number: 2001
Flags: ACK
Window Size: 65535
Checksum: 0x3E9D
Payload: (none)
"Connection established. I'm ready to send data starting at byte 1001."
After the handshake:
Client's data starts at seq 1001
Server's data starts at seq 2001
Note that even these empty-payload segments carry a checksum — it's computed over the header and a pseudo-header (source/destination IPs, protocol, length), so every segment is checked for corruption, not just ones carrying data.
Phase 2: Data Transfer
Sending the message
socket.write("hello hello hello hello hello "); // 30 bytes, assume MSS = 6TCP splits this into 5 segments of 6 bytes each:
| Segment | Seq | Payload | Bytes covered | Checksum |
|---|---|---|---|---|
| 1 | 1001 | "hello " | 1001–1006 | 0x4F2A |
| 2 | 1007 | "hello " | 1007–1012 | 0x88B1 |
| 3 | 1013 | "hello " | 1013–1018 | 0x91C7 |
| 4 | 1019 | "hello " | 1019–1024 | 0x2D5E |
| 5 | 1025 | "hello " | 1025–1030 | 0x6603 |
Each segment header looks like this (only Sequence Number, Checksum, and payload change):
Source Port: 54321
Destination Port: 8080
Acknowledgment Number: 2001
Flags: PSH, ACK
Window Size: 65535
Reassembly at the server
Receives, in order: [1001]"hello " [1007]"hello " [1013]"hello " [1019]"hello " [1025]"hello "
Each segment's checksum is verified on arrival before being placed in the buffer.
Reassembly buffer: "hello hello hello hello hello "
-> Passed to application as: "hello hello hello hello hello "
The application never sees the splitting — socket.on('data', ...) just receives the full string (possibly across one or more data events, but TCP guarantees the byte order is correct, and the checksum guarantees each segment arrived intact).
Checksum: Catching Corrupted Segments
Before looking at how ACKs and retransmission work, it's worth understanding what triggers them in the first place — not just network loss, but corruption. Every TCP segment carries a Checksum field, computed over the header and payload (plus a pseudo-header containing source/destination IPs) before sending. The receiver recomputes the checksum on arrival and compares it to the value in the header.
Segment sent successfully:
Client computes checksum over Segment 1 (seq 1001, "hello "):
Checksum: 0x4F2A
Segment 1 --> arrives at Server
Server recomputes checksum over received bytes: 0x4F2A
Match -> segment accepted, processing continues normally.
Segment corrupted in transit (e.g. a bit flips on the wire):
Client computes checksum over Segment 3 (seq 1013, "hello "):
Checksum: 0x91C7
Segment 3 --> bit flips in transit, payload now reads "hellp "
Server recomputes checksum over received bytes: 0x91D3
Mismatch (0x91C7 != 0x91D3) -> segment silently DISCARDED.
No ACK is sent for seq 1013. From the server's perspective, it's as if
Segment 3 never arrived at all.
This is the key point: a checksum failure looks identical to a lost segment. The server simply doesn't acknowledge seq 1013, so the exact same duplicate-ACK / retransmission machinery used for true network loss kicks in — there's no separate "corruption recovery" path. This is exactly what Scenario 3 below walks through.
| Layer | What it catches | What happens on failure |
|---|---|---|
| TCP Checksum | Bit-level corruption in header or payload | Segment discarded, treated as lost, triggers retransmission |
| Sequence numbers | Missing or duplicate segments | Duplicate ACK / gap detection |
| ACK timeout | No response at all (true network loss) | Sender retransmits after timeout |
Acknowledgment Strategies: Immediate vs Delayed vs Cumulative
Scenario 1 — Immediate ACK per segment (strict, high overhead)
Seg1 (seq 1001) --> <-- ACK ack=1007
Seg2 (seq 1007) --> <-- ACK ack=1013
Seg3 (seq 1013) --> <-- ACK ack=1019
Seg4 (seq 1019) --> <-- ACK ack=1025
Seg5 (seq 1025) --> <-- ACK ack=1031
10 messages total (5 data + 5 ACK). Each ACK means "I received up to X (checksum verified), next expected is Y."
Scenario 2 — Delayed / cumulative ACK (what real TCP does)
Seg1 --> Seg2 --> Seg3 --> Seg4 --> Seg5 -->
(server waits ~40ms for more data)
<-- ACK ack=1031 (cumulative)
6 messages total (5 data + 1 ACK). A single ack=1031 means "I received and verified ALL 30 bytes from 1001–1030; next expected byte is 1031."
Scenario 3 — Out-of-order / lost or corrupted segment (Segment 3 missing)
Seg1 (1001) --> <-- ACK ack=1007 (or delayed)
Seg2 (1007) -->
Seg3 (1013) --> [LOST in network, OR checksum mismatch -> discarded]
Seg4 (1019) --> <-- ACK ack=1013 (duplicate ACK — signals Seg3 missing!)
Seg5 (1025) --> <-- ACK ack=1013 (duplicate ACK again)
Client retransmits Seg3 (seq 1013) --> checksum verified this time
--> <-- ACK ack=1031 (all 30 bytes now received)
Duplicate ACKs for the same sequence number are TCP's signal that a segment is missing and needs retransmission — and as covered above, the receiver can't tell (and doesn't need to know) whether the original was lost in the network or arrived corrupted and failed its checksum. Either way, the fix is identical: retransmit. Modern TCP can also use SACK (Selective ACK) to acknowledge non-contiguous ranges directly — e.g. the server could ACK "I have 1019–1030, just missing 1013–1018," letting the client retransmit only the gap instead of guessing.
ACK Rules Summary
| Rule | Details |
|---|---|
| Cumulative | An ACK acknowledges all bytes received and checksum-verified up to that sequence number |
| Delayed | Receiver may wait up to ~40–50ms before ACKing, hoping to batch |
| Forced ACK | If a new segment arrives while delaying, an ACK must go out within ~1 RTT |
| Duplicate ACK | Repeated ACK for the same number signals a gap (lost or corrupted segment) — triggers retransmission |
| SACK | Can acknowledge non-contiguous segments when reordering occurs |
Flow Control: Window Size
Every segment and ACK carries a Window Size field — "I have this much buffer space, send up to this much more before I ACK again." This protects a slow receiver from being overwhelmed by a fast sender.
Buffer fills up:
Server's receive buffer is full.
Server --> ACK ack=1031, Window Size: 0
"I've received and verified everything so far, but my buffer is full — stop sending." The client pauses, even though it may have more data queued.
Buffer frees up — window update:
Server's application reads data out of the buffer, freeing space.
Server --> ACK ack=1031, Window Size: 65535 (window update probe)
"I have room again — you may resume sending." The client resumes transmission. This Window Size: 0 → window update cycle is how TCP throttles a sender to match a receiver's processing speed, without either side closing the connection.
Phase 3: Connection Termination (4-Step FIN/ACK)
After all 30 bytes are sent, checksum-verified, and acknowledged (ack=1031), the client has no more data and initiates termination.
Client Server
| FIN, seq=1031 |
| ------------------------------>
| ACK, ack=1032 |
| <------------------------------
| FIN, seq=2001 |
| <------------------------------
| ACK, ack=2002 |
| ------------------------------>
Step 1 — Client FIN
Sequence Number: 1031
Acknowledgment Number: 2001
Flags: FIN, ACK
Checksum: 0x1A77
"I have no more data to send."
Step 2 — Server ACKs the FIN
Sequence Number: 2001
Acknowledgment Number: 1032
Flags: ACK
Checksum: 0x5BD0
"Understood — you're done sending." (The server's side may still have data to send, so it doesn't FIN immediately.)
Step 3 — Server's own FIN
Sequence Number: 2001
Acknowledgment Number: 1032
Flags: FIN, ACK
Checksum: 0xE942
"I'm done sending too."
Step 4 — Client ACKs the server's FIN
Sequence Number: 1032
Acknowledgment Number: 2002
Flags: ACK
Checksum: 0x0C56
"Acknowledged. Connection fully closed."
Like every other segment in this connection, these four are checksummed too — a corrupted FIN or ACK here would be discarded just like a corrupted data segment, and handled by the same retransmission logic described next.
Why TIME_WAIT exists
The client now enters TIME_WAIT for a short period (typically ~30–120 seconds) instead of immediately discarding the connection. Here's why, using the same loss-and-retransmission logic from Phase 2:
Step 4's ACK (ack=2002) is lost in the network (or arrives corrupted and fails checksum).
Server never receives a valid ACK -> Server retransmits its FIN (seq 2001, Flags: FIN, ACK).
If the client had already discarded the connection state, it would have
no way to respond — and might even reply with a RST, confusing the server.
Because the client is in TIME_WAIT, it still recognizes the retransmitted
FIN and re-sends ACK ack=2002.
TIME_WAIT is the same "what if the last message gets lost or corrupted" problem you saw with data segments in Phase 2 — just applied to the final handshake of termination.
Code Example: What the Application Sees
// Server
const net = require("net");
const server = net.createServer((socket) => {
socket.on("data", (chunk) => {
console.log("Received:", chunk.toString());
// TCP has already handled splitting, reordering, checksum verification,
// retransmission, flow control, and ACKing behind the scenes.
});
});
server.listen(8080);// Client
const socket = net.createConnection({ port: 8080 });
socket.write("hello "); // may become Segment 1
socket.write("hello "); // may become Segment 2
socket.write("hello "); // ...and so on
// TCP stack handles checksums, ACKs, retransmits, window sizing,
// and reassembly automatically.
socket.end(); // triggers the FINFull Lifecycle Summary (One Connection)
PHASE 1: Establishment
Client --SYN(seq=1000)-------> Server
Client <--SYN,ACK(seq=2000,ack=1001)-- Server
Client --ACK(seq=1001,ack=2001)------> Server
(every segment above carries a checksum over header + pseudo-header)
PHASE 2: Data Transfer
seq 1001 "hello " (checksum 0x4F2A) -->
seq 1007 "hello " (checksum 0x88B1) --> } cumulative ACK ack=1031 (delayed ~40ms)
seq 1013 "hello " (checksum 0x91C7) --> } or duplicate ACKs + retransmit if a segment
seq 1019 "hello " (checksum 0x2D5E) --> } is lost OR fails its checksum
seq 1025 "hello " (checksum 0x6603) --> } Window Size 0 / window update if receiver buffer fills
PHASE 3: Termination
Client --FIN(seq=1031)----------------> Server
Client <--ACK(ack=1032)--------------- Server
Client <--FIN(seq=2001)---------------- Server
Client --ACK(ack=2002)----------------> Server
Client enters TIME_WAIT (in case the last ACK or server's FIN is lost,
corrupted, or retransmitted)
Every field — source/destination port, sequence number, acknowledgment number, flags, window size, and checksum — does real work across all three phases. Tracing one byte stream through establishment, transfer (with corruption detection, loss, ACK strategies, and flow control), and termination (with its own loss-recovery story via TIME_WAIT) is what turns the TCP header from an abstract table into a story you can follow from end to end.