Skip to content
Menu

Networking8 min read

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.

Source Port 16 bits
Destination Port 16 bits
Sequence Number 32 bits
Acknowledgment Number 32 bits
Data Offset 4 bits
Reserved 3 bits
Flags: NS CWR ECE URG ACK PSH RST SYN FIN 9 bits
Window Size 16 bits
Checksum 16 bits
Urgent Pointer 16 bits
Options + Padding variable (0-320 bits)

Header Field Roles

FieldPurpose
Sequence NumberByte offset of the first data byte in this segment within the stream
Acknowledgment NumberNext byte the sender expects to receive (cumulative ACK)
Data OffsetHeader length in 32-bit words (min 5 = 20 bytes, max 15 = 60 bytes)
Window SizeReceive buffer space available (flow control)
ChecksumCovers header + data + pseudo-header (source/dest IP, protocol, length)
Urgent PointerOffset to end of urgent data (rarely used, mostly legacy)

TCP Flags Breakdown

FlagMeaningWhen Used
SYNSynchronize sequence numbersConnection establishment
ACKAcknowledgment field is validAlmost every segment after SYN
FINNo more data from senderConnection teardown
RSTReset the connectionAbort, rejected connection, half-open detection
PSHPush data to application immediatelyInteractive traffic (SSH keystrokes)
URGUrgent pointer field is validOut-of-band data (rarely used today)
ECEECN-EchoExplicit Congestion Notification
CWRCongestion Window ReducedSender acknowledges ECE
NSNonce SumECN 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).

Client Server
SYN   seq=1000
SYN+ACK   seq=5000, ack=1001
ACK   seq=1001, ack=5001

What Happens at Each Step

  1. SYN: Client picks a random Initial Sequence Number (ISN = 1000) and sends it. The SYN itself consumes one sequence number.
  2. SYN+ACK: Server picks its own ISN (5000), acknowledges the client's ISN+1 (ack=1001), and sends both flags in one segment.
  3. 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=1001 and 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=3000 means "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)
Sender Receiver
seq=1001, len=500 (bytes 1001-1500)
seq=1501, len=500 (bytes 1501-2000)
ack=2001 (got everything up to 2000)

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 rwnd bytes of unacknowledged data in flight
  • As the receiver processes data and frees buffer space, it increases rwnd in 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.

console
console

    $ 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)

  1. Slow Start

    cwnd doubles every RTT (exponential growth)

  2. Congestion Avoidance

    cwnd += 1 MSS per RTT (linear growth)

  3. Loss Detected

    Timeout or 3 duplicate ACKs

  4. Recovery

    Adjust cwnd, retransmit

Phases in Detail

PhaseTriggercwnd Behaviorssthresh 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

CUBIC (Linux Default)
  • 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)
BBR (Google)
  • 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
console
console

    # 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.

Nagle Enabled (default)
  • Reduces small-packet overhead (fewer headers per payload byte)
  • Good for bulk transfers, general-purpose traffic
  • Reduces network congestion from "tinygram" floods
Nagle Disabled (TCP_NODELAY)
  • 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_NODELAY on the sender — disables Nagle, send immediately. Most common fix.
  • TCP_QUICKACK on 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.

OptionSizePurposeNegotiated 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.

console
console

    # 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.

Solidnines — solidnines.com