TCP Congestion Control — Deep Dive
TCP congestion control is the sender-side mechanism that prevents any single TCP flow from overwhelming the network. The key state variable is cwnd (congestion window) — an internal limit at the sender that caps how much unacknowledged data can be in flight at once.
Core State Variables
| Variable | Meaning |
|---|---|
cwnd |
Congestion window — sender's self-imposed limit (in MSS units) |
ssthresh |
Slow start threshold — boundary between slow start and congestion avoidance |
rwnd |
Receiver's advertised window — from TCP header |
MSS |
Maximum Segment Size — typically 1460 bytes for 1500 MTU |
Effective window = min(cwnd, rwnd) — the actual cap on in-flight data.
Phase 1: Slow Start
Initial: cwnd = 1 MSS (or 10 MSS per RFC 6928), ssthresh = 65535 (or system default)
Per ACK received: cwnd += 1 MSS
Per RTT: cwnd doubles (exponential growth)
Stop when: cwnd >= ssthresh → switch to congestion avoidance
OR loss detected → react based on signal type
Slow start isn't slow — it grows exponentially. At 100ms RTT, cwnd hits 1MB in about 10 RTTs (~1 second).
Phase 2: Congestion Avoidance (AIMD)
Once cwnd >= ssthresh, switch to additive increase:
Per ACK: cwnd += MSS * MSS / cwnd # ≈ 1 MSS per RTT
Per RTT: cwnd += 1 MSS # linear growth
This is Additive Increase. The window grows by one MSS per round trip instead of doubling.
Loss Detection and Response
Timeout (severe loss signal)
Reaction:
ssthresh = max(cwnd/2, 2*MSS)
cwnd = 1 MSS
re-enter slow start
This is brutal — drops cwnd to 1. Happens when the retransmit timer fires before 3 dup ACKs arrive.
Triple Duplicate ACK (mild loss signal)
Three dup ACKs mean one segment was lost but later ones arrived. Network isn't congested, just a drop.
TCP Reno Fast Recovery:
ssthresh = max(cwnd/2, 2*MSS)
cwnd = ssthresh + 3*MSS # 3 for the 3 dup ACKs
Fast retransmit lost segment
For each additional dup ACK: cwnd += 1 MSS (inflate window)
On new ACK: cwnd = ssthresh, exit fast recovery
AIMD — The Core Principle
Additive Increase, Multiplicative Decrease:
- Increase: Add 1 MSS per RTT when things are good (linear)
- Decrease: Halve cwnd on congestion signal (multiplicative)
This is provably fair — if N flows share a bottleneck, each converges to 1/N of the capacity. It also converges even if flows start at different times.
CUBIC (Linux Default)
CUBIC replaces the linear increase with a cubic function of time since last loss event:
W(t) = C * (t - K)^3 + Wmax
Where:
Wmax = window at last loss point
K = time to reach Wmax again = (Wmax * β / C)^(1/3)
C = scaling constant (0.4)
β = 0.7 (multiplicative decrease factor, vs 0.5 in Reno)
CUBIC grows more aggressively than Reno at high bandwidth-delay products (e.g., trans-oceanic links). Less conservative than Reno at recovering window after loss.
# Verify CUBIC is active (Linux)
sysctl net.ipv4.tcp_congestion_control
# -> cubic
# See CUBIC-specific stats
ss -tin
# Look for: cwnd, ssthresh, rtt, mss in output
BBR — Bottleneck Bandwidth and RTT
BBR (Google, 2016) takes a completely different approach. Instead of reacting to loss, it models the network:
Bottleneck Bandwidth (BtlBw) = max delivery rate observed
RTprop = min RTT observed
cwnd = BtlBw * RTprop (the BDP — bandwidth delay product)
BBR alternates between probing for more bandwidth and draining the queue. It doesn't treat packet loss as the primary congestion signal — it targets the BDP directly.
# Enable BBR on Linux (requires kernel 4.9+)
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
sudo sysctl -w net.core.default_qdisc=fq # BBR needs fair queue scheduler
# Verify
sysctl net.ipv4.tcp_congestion_control
# -> bbr
# Watch BBR parameters on a live socket
ss -tin dst <ip>
# Output: bbrv2 style: pacing_rate, cwnd, delivery_rate
Observing Congestion Control with ss
# Detailed TCP socket stats
ss -tin
# Example output breakdown:
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# ESTAB 0 0 192.168.1.5:45231 1.2.3.4:443
# cubic wscale:7,7 rto:200 rtt:12.4/3.1 ato:40
# mss:1448 pmtu:1500 rcvmss:1448 advmss:1448
# cwnd:10 ssthresh:10 bytes_sent:5242880 bytes_retrans:1448
# segs_out:3620 segs_in:1810 send 9.3Mbps lastsnd:0 lastrcv:0 lastack:4
# Key fields:
# cwnd:10 = 10 MSS in flight allowed
# ssthresh:10 = at threshold boundary
# bytes_retrans = total retransmitted bytes (sign of congestion)
# send 9.3Mbps = current sending rate
# Monitor live
watch -n 1 "ss -tin dst <target_ip> | grep -E 'cwnd|rtt|ssthresh'"
How Attackers Exploit Congestion Control
SYN Flood
# Classic SYN flood exhausts server half-open connection queue
# cwnd is never reached — connection never completes
hping3 -S --flood -V -p 80 <target>
# Mitigation: SYN cookies
sudo sysctl -w net.ipv4.tcp_syncookies=1
Congestion Window Manipulation
# Concept: spoofed duplicate ACKs can trigger fast retransmit
# and cause the victim sender to back off cwnd
# Requires ability to inject ACK packets (on-path or via spoofing if sequence predictable)
Low-Rate DDoS (Shrew Attack)
Send periodic bursts timed to match TCP retransmit timeout (RTO):
- Burst causes packet loss → TCP backs off (cwnd halves, slow start)
- Wait for RTO (~200ms-1s), burst again
- Maintains severe throughput reduction with minimal attacker bandwidth
ACK Throttling Defense
# Detect abnormal retransmit rates
netstat -s | grep -i retransmit
# or
cat /proc/net/netstat | grep -i retrans
Tuning for High-Performance (not attack use)
# Increase TCP buffer sizes for high-bandwidth links
sudo sysctl -w net.core.rmem_max=134217728
sudo sysctl -w net.core.wmem_max=134217728
sudo sysctl -w net.ipv4.tcp_rmem="4096 87380 67108864"
sudo sysctl -w net.ipv4.tcp_wmem="4096 65536 67108864"
# Enable window scaling (should be on by default)
sudo sysctl -w net.ipv4.tcp_window_scaling=1
# Enable SACK (selective acknowledgment)
sudo sysctl -w net.ipv4.tcp_sack=1
Reference
- RFC 5681 — TCP Congestion Control
- RFC 6928 — Initial Congestion Window of 10
- RFC 8312 — CUBIC
- BBR paper: https://queue.acm.org/detail.cfm?id=3022184
ssman page:man ss