Frontend Labs
⋯

29. Fast retransmit, fast recovery, and SACK

29.1 Fast retransmit

Rule: on the 3rd duplicate ACK (same ack number, no new data, zero window change), retransmit the segment starting at that ack number immediately — don't wait for the RTO.

Why 3 and not 1? To tolerate mild reordering: 1–2 dup-ACKs commonly just mean packets arrived slightly out of order. 3 is a heuristic threshold (RACK replaces it with time, §27.5).

29.2 Fast recovery (the "half, not zero" idea)

A timeout means "the network might be dead" → drop congestion window to 1 (restart slow start). But three dup-ACKs mean "packets are still flowing" (the receiver got later segments), so the reaction is gentler:

TCP Reno fast recovery (RFC 5681):

  1. On 3rd dup-ACK: ssthresh = max(FlightSize / 2, 2*MSS); retransmit the lost segment; cwnd = ssthresh + 3*MSS (the 3 accounts for the 3 segments that left the network — that's what generated the dup-ACKs).
  2. For each additional dup-ACK: cwnd += MSS ("inflate" — more segments have left the network, so you may inject one).
  3. When a new ACK finally arrives (the retransmit got through): cwnd = ssthresh ("deflate") and resume congestion avoidance (linear growth), not slow start.

Net effect: one loss ≈ halve the rate, then keep going. This is the "sawtooth" of classic TCP throughput.

29.3 Why plain Reno struggles with multiple losses per window

Reno's dup-ACK counting only tells the sender about one hole at a time. Lose two segments in the same window and Reno often needs a second recovery round (or an RTO). NewReno (RFC 6582) patches this: it remembers the highest sequence sent when recovery began ("recovery point") and stays in fast recovery, retransmitting one segment per RTT, until all data up to that point is ACKed — no RTO needed for multiple losses, but still only one repair per RTT.

29.4 SACK — Selective Acknowledgment (RFC 2018), the real fix

Negotiated by SACK-permitted on both SYNs. Then a receiver with holes sends SACK blocks in the ACK's options: up to 3–4 pairs of (left edge, right edge) describing contiguous ranges it has received above the cumulative ack.

Cumulative ACK = 2001
SACK: 3001-4001, 5001-6001
Meaning: "I have through 2000, AND 3001-4000, AND 5001-6000.
          I'm missing 2001-3000 and 4001-5000."

The sender now retransmits exactly the two missing ranges, in one RTT, without guessing. Combined with a scoreboard (per-segment SACKed/lost state) and RACK for loss detection, this is how modern Linux TCP recovers from bursty loss efficiently.

  • DSACK (RFC 2883): the receiver also reports ranges it received twice ("you retransmitted unnecessarily"), so the sender can raise its reordering estimate / undo a spurious cwnd cut.
  • FACK / RACK: heuristics/algorithms for deciding, from SACK info, how much data is actually "lost" vs merely "in flight," to set cwnd and what to resend.

29.5 PRR — Proportional Rate Reduction (RFC 6937)

Replaces Reno's abrupt "halve then inflate/deflate" during recovery with a smooth, ACK-clocked reduction that converges cwnd to ssthresh by the end of recovery without the bursty inflate/deflate. Default in Linux. It's why modern TCP recovers with fewer secondary losses.

29.6 War story

A backup over a satellite link (RTT ~600 ms, occasional 1–2% bursty loss) crawled at 3 Mbit/s on a 50 Mbit/s pipe. Capture: SACK was disabled because a "security" middlebox stripped the SACK-permitted option from the SYN. Every loss event forced NewReno to repair one segment per 600 ms RTT — catastrophic on a long fat pipe. Whitelisting the option through the middlebox (and enabling tcp_sack, tcp_dsack) took the backup to ~40 Mbit/s. Lesson: on high-latency or lossy paths, no SACK = no throughput; verify both SYNs carry sackOK.

Try it yourself

sysctl net.ipv4.tcp_sack net.ipv4.tcp_dsack net.ipv4.tcp_fack
nstat -az | grep -Ei 'sack|dsack|renorecovery|sackrecovery|prr|tcplostretransmit'
# In a capture, Wireshark: filter  tcp.analysis.flags  - flags duplicate ACKs,
# fast retransmits, out-of-order, spurious retransmits, zero windows.
sudo tcpdump -i any -n 'tcp[tcpflags] & tcp-ack != 0' -v | grep -m5 'sack'

Generated from real-life-example.md · 8 Oct 2026 16:36