TCP Deep Dive
Reliable, ordered, connection-oriented transport
TCP Segment Header
Every TCP segment carries a 20-byte minimum header (up to 60 bytes with options). This header is the control plane of the entire protocol — sequence tracking, acknowledgments, flow control, and connection management all live here.
Header Field Roles
| Field | Purpose |
|---|---|
Sequence Number | Byte offset of the first data byte in this segment within the stream |
Acknowledgment Number | Next byte the sender expects to receive (cumulative ACK) |
Data Offset | Header length in 32-bit words (min 5 = 20 bytes, max 15 = 60 bytes) |
Window Size | Receive buffer space available (flow control) |
Checksum | Covers header + data + pseudo-header (source/dest IP, protocol, length) |
Urgent Pointer | Offset to end of urgent data (rarely used, mostly legacy) |
TCP Flags Breakdown
| Flag | Meaning | When Used |
|---|---|---|
SYN | Synchronize sequence numbers | Connection establishment |
ACK | Acknowledgment field is valid | Almost every segment after SYN |
FIN | No more data from sender | Connection teardown |
RST | Reset the connection | Abort, rejected connection, half-open detection |
PSH | Push data to application immediately | Interactive traffic (SSH keystrokes) |
URG | Urgent pointer field is valid | Out-of-band data (rarely used today) |
ECE | ECN-Echo | Explicit Congestion Notification |
CWR | Congestion Window Reduced | Sender acknowledges ECE |
NS | Nonce Sum | ECN nonce protection (experimental) |
3-Way Handshake
TCP connections are full-duplex and require a negotiation before data flows. The 3-way handshake synchronizes sequence numbers in both directions and negotiates options (MSS, window scaling, SACK, timestamps).
What Happens at Each Step
- SYN: Client picks a random Initial Sequence Number (ISN = 1000) and sends it. The SYN itself consumes one sequence number.
- SYN+ACK: Server picks its own ISN (5000), acknowledges the client's ISN+1 (
ack=1001), and sends both flags in one segment. - ACK: Client acknowledges the server's ISN+1 (
ack=5001). Connection is ESTABLISHED. Data can now flow in both directions.
Note
Why random ISNs? Predictable ISNs allow TCP sequence prediction attacks (blind spoofing). Modern kernels use cryptographically random ISNs (RFC 6528).
Sequence & Acknowledgment Numbers
A critical distinction: TCP sequence numbers count bytes, not packets. TCP is a byte-stream protocol — the receiver reconstructs an ordered byte stream, not a sequence of messages.
Byte-Stream Semantics
- If
seq=1001and the segment carries 500 bytes of data, the bytes span positions 1001-1500 - The receiver's ACK will be
ack=1501— "I have all bytes up to 1500, send me byte 1501 next" - ACKs are cumulative:
ack=3000means "I have everything up to byte 2999" - Out-of-order segments are buffered by the receiver and ACKed only when the gap is filled (unless SACK is in use)
Tip
Wireshark note: Wireshark shows "relative sequence numbers" by default (starting from 0). The actual ISN is random and 32-bit. Toggle this in Edit > Preferences > Protocols > TCP.
Flow Control: Sliding Window
Flow control prevents a fast sender from overwhelming a slow receiver. The receiver advertises how much buffer space it has available — the receive window (rwnd) — in every ACK segment's Window Size field.
Sliding Window Mechanism
- The sender can have at most
rwndbytes of unacknowledged data in flight - As the receiver processes data and frees buffer space, it increases
rwndin subsequent ACKs - If
rwnd=0, the sender must stop and periodically send window probes (1-byte segments) to check if space has opened up - The Window Size field is 16 bits = max 65,535 bytes. On modern networks, this is far too small
Window Scaling Option (RFC 7323)
Negotiated during the handshake via TCP options. The scale factor is a power of 2 (0-14), allowing windows up to 65,535 x 2^14 = ~1 GB.
$ ss -i dst 10.0.0.5
ESTAB 0 0 10.0.0.1:44832 10.0.0.5:443
cubic wscale:7,7 rto:204 rtt:1.5/0.5 ato:40 mss:1448
rcvmss:1448 advmss:1448 cwnd:10 ssthresh:7
bytes_sent:1024 bytes_acked:1025 bytes_received:8192
send 77.2Mbps rcv_space:14480 rcv_ssthresh:64088
Note
wscale:7,7 means both sides use a scale factor of 7 (multiply the Window Size field by 128). Effective max window = 65,535 x 128 = ~8 MB.
Congestion Control
Flow control protects the receiver. Congestion control protects the network. The sender maintains a congestion window (cwnd) that limits how much unacknowledged data can be in flight. The effective window is min(cwnd, rwnd).
Classic Congestion Control (Reno/NewReno)
Slow Start
cwnd doubles every RTT (exponential growth)
Congestion Avoidance
cwnd += 1 MSS per RTT (linear growth)
Loss Detected
Timeout or 3 duplicate ACKs
Recovery
Adjust cwnd, retransmit
Phases in Detail
| Phase | Trigger | cwnd Behavior | ssthresh Change |
|---|---|---|---|
| Slow Start | Connection start, or timeout recovery | cwnd starts at IW (typically 10 MSS on Linux), doubles each RTT | N/A (continues until cwnd >= ssthresh) |
| Congestion Avoidance | cwnd >= ssthresh | cwnd grows by ~1 MSS per RTT (additive increase) | N/A |
| Fast Retransmit | 3 duplicate ACKs received | Retransmit the lost segment immediately (don't wait for timeout) | ssthresh = cwnd / 2 |
| Fast Recovery | After fast retransmit | cwnd = ssthresh + 3 MSS, then congestion avoidance (skip slow start!) | Already set by fast retransmit |
| Timeout | Retransmission timer expires | cwnd = 1 MSS, back to slow start (the nuclear option) | ssthresh = cwnd / 2 |
Warning
Why fast recovery matters: A timeout is catastrophic — cwnd drops to 1 MSS. Fast retransmit + fast recovery detect loss via duplicate ACKs and cut cwnd only in half, skipping the slow start phase entirely. This is the difference between a brief throughput dip and a complete stall.
Modern Congestion Control Algorithms
- Loss-based — reacts to packet loss
- Uses a cubic function for window growth (not linear)
- After loss: cwnd drops, then rapidly recovers to the pre-loss level, then grows cautiously beyond
- Better than Reno for high-BDP (bandwidth-delay product) networks
- Default on Linux since kernel 2.6.19 (2006)
- Problem: fills buffers before detecting loss (bufferbloat)
- Model-based — estimates bottleneck bandwidth and RTT
- Tries to send at the rate the bottleneck can handle, no more
- Does NOT use loss as the primary signal
- Significantly better on lossy links (wireless, intercontinental)
- Available on Linux since kernel 4.9 (2016)
- BBRv2 addresses fairness issues with CUBIC coexistence
# Check current congestion control algorithm
$ sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = cubic
# Switch to BBR (requires kernel >= 4.9)
$ sysctl -w net.ipv4.tcp_congestion_control=bbr
$ sysctl -w net.core.default_qdisc=fq
Tip
DevOps tip: If you run services over high-latency links (cross-region, CDN origins), switching to BBR can dramatically improve throughput. Many cloud providers (GCP, some AWS instances) already default to BBR.
Nagle's Algorithm
Nagle's algorithm (RFC 896) coalesces small writes into larger segments to reduce the overhead of tiny packets on the network. The rule is simple:
Nagle's Rule
If there is unacknowledged data in flight AND the new data is smaller than MSS, buffer it — wait for the ACK (or enough data to fill an MSS) before sending.
If there is no unacknowledged data in flight, send immediately regardless of size.
- Reduces small-packet overhead (fewer headers per payload byte)
- Good for bulk transfers, general-purpose traffic
- Reduces network congestion from "tinygram" floods
- Every
write()sends immediately - Essential for latency-sensitive apps: SSH, gaming, real-time APIs
- Required when application does its own buffering (e.g., HTTP/2 framing)
Delayed ACKs
Rather than ACKing every segment immediately, the receiver can delay the ACK up to ~200ms (Linux default: 40ms), hoping to piggyback the ACK on a response data segment. This reduces the number of pure-ACK packets on the wire.
Warning
The Nagle + Delayed ACK Problem: When Nagle is enabled on the sender and delayed ACKs on the receiver, you get a pathological interaction. The sender has a small write buffered (Nagle is waiting for the ACK). The receiver has data to ACK but is delaying (waiting to piggyback). Both sides wait for each other — the result is a 40-200ms latency spike on every small write. This is one of the most common TCP performance gotchas.
Solutions
TCP_NODELAYon the sender — disables Nagle, send immediately. Most common fix.TCP_QUICKACKon the receiver (Linux-specific) — disables delayed ACKs temporarily. Less common.- Application-level buffering: batch small writes into a single
write()call, then let it send as one segment.
Tip
Rule of thumb: If your application is request-response (HTTP, gRPC, database protocols), set TCP_NODELAY. If your application streams bulk data, leave Nagle enabled.
TCP Options
TCP options are negotiated during the handshake (in SYN and SYN-ACK segments) and carried in the variable-length Options field of the header.
| Option | Size | Purpose | Negotiated In |
|---|---|---|---|
| MSS (Maximum Segment Size) | 4 bytes | Largest payload each side can receive (typically 1460 bytes for Ethernet: 1500 MTU - 20 IP - 20 TCP) | SYN, SYN-ACK |
| Window Scale | 3 bytes | Multiplier for the 16-bit Window Size field (scale factor 0-14, allows windows up to ~1 GB) | SYN, SYN-ACK |
| SACK Permitted | 2 bytes | Enables Selective Acknowledgment — receiver can report non-contiguous blocks it has received | SYN, SYN-ACK |
| SACK Blocks | Variable | Actual SACK data: up to 4 block ranges of received-but-not-yet-acked bytes | Any segment |
| Timestamps | 10 bytes | TSval/TSecr: enables precise RTT measurement and PAWS (Protection Against Wrapped Sequence numbers) | SYN, SYN-ACK (then every segment) |
Note
SACK in practice: Without SACK, a single lost segment forces the sender to retransmit everything from the lost segment onward (go-back-N). With SACK, the sender knows exactly which segments arrived and retransmits only the gaps. This is critical for performance on lossy networks. SACK is enabled by default on virtually all modern OS kernels.
# Check SACK and timestamps settings on Linux
$ sysctl net.ipv4.tcp_sack
net.ipv4.tcp_sack = 1
$ sysctl net.ipv4.tcp_timestamps
net.ipv4.tcp_timestamps = 1
$ sysctl net.ipv4.tcp_window_scaling
net.ipv4.tcp_window_scaling = 1
Tip
Troubleshooting tip: If you see poor TCP performance through a middlebox (load balancer, firewall), check whether it strips TCP options. Some older devices strip timestamps or window scaling, silently crippling performance.