Networking

TCP Selective Acknowledgment (SACK): Recovering Multiple Losses Without Retransmitting the Whole Window

Lose four packets from a 100-segment window and a classic cumulative-ACK TCP sender learns about exactly one of them per round trip — so recovery costs four RTTs, and on a satellite link that is a full second of stalled throughput. TCP Selective Acknowledgment (SACK) collapses that to a single RTT by letting the receiver hand the sender a precise map of which byte ranges actually arrived.

SACK, defined in RFC 2018 (Mathis, Mahdavi, Floyd & Romanow, October 1996), is a pair of TCP options: a one-time SACK-Permitted flag exchanged in the SYN handshake, and a SACK option carried on ACKs that lists up to four contiguous blocks of out-of-order data the receiver has already queued. The cumulative ACK still advances the left edge of the window; SACK fills in the holes above it, so the sender retransmits only the true gaps instead of everything past the first loss.

  • TypeTCP option / loss-recovery mechanism
  • InventedRFC 2018, October 1996 (Mathis, Mahdavi, Floyd, Romanow)
  • Option kindsKind 4 = SACK-Permitted, Kind 5 = SACK
  • Max blocks per ACK4 (3 with TCP timestamps enabled)
  • Losses recovered per RTTMultiple (vs. 1 for cumulative ACK)
  • Used inLinux, Windows, macOS, BSD TCP stacks; QUIC ACK ranges

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

The problem: cumulative ACKs blind the sender to all but the first hole

TCP's baseline reliability is a cumulative acknowledgment: an ACK carrying number N means "I have every byte below N, in order." It says nothing about bytes above N. If segments 5, 8, and 12 are lost from a window, the receiver keeps ACKing byte-position 5 (the first hole) no matter how much later data arrives and gets buffered.

A classic Reno sender interprets the stream of duplicate ACKs as "segment 5 is missing," fast-retransmits it, and only after that segment is recovered does the ACK advance far enough to expose the next hole at 8. Each hole therefore costs roughly one round trip.

  • Symptom: N losses in one window take ~N RTTs to recover.
  • Worse case: if the sender exhausts its window before recovery, it stalls into a retransmission timeout (RTO) and collapses cwnd to 1.

This is exactly the regime where high bandwidth-delay-product links — satellite, transcontinental, lossy Wi-Fi — bleed throughput. SACK exists to give the sender the whole gap map at once.

How it works: SACK-Permitted, SACK blocks, and the sender scoreboard

SACK is two TCP options negotiated and used in three phases:

  • Negotiation: each side puts a SACK-Permitted option (Kind 4, length 2) in its SYN. SACK is used only if both SYNs carry it.
  • Reporting: a receiver holding out-of-order data sends ACKs carrying the SACK option (Kind 5). Its payload is a list of blocks, each a pair of 32-bit sequence numbers — left edge (first byte of the block) and right edge (first byte after the block). The cumulative ACK field still reports the in-order boundary; the blocks describe islands of received data above it. RFC 2018 requires the first block to cover the most recently received segment.
  • Acting: the sender maintains a scoreboard, a per-segment record marking bytes as SACKed or still missing. It retransmits only ranges that are missing and whose loss is confirmed (typically by 3 SACKed segments above them, per RFC 6675's dupack/SACK-based recovery).
SACK option layout (bytes):
 kind=5 | length | Left#1 | Right#1 | Left#2 | Right#2 | ...
 1B 1B 4B 4B 4B 4B

Crucially, SACK is advisory: only the cumulative ACK is a firm promise. A receiver may later discard SACKed data (reneging), so the sender keeps segments buffered until the cumulative ACK covers them.

Complexity, the 4-block ceiling, and a worked step-trace

SACK's cost is dominated not by CPU but by the 40-byte TCP option budget. Each SACK block is 8 bytes and the option header is 2 bytes, so the theoretical maximum is floor((40 - 2) / 8) = 4 blocks. With the common TCP Timestamps option present (10 bytes + 2 NOP padding = 12 bytes), only floor((40 - 12 - 2) / 8) = 3 blocks fit. Scoreboard operations are O(number of blocks/holes) per ACK — effectively O(1) amortized with a well-implemented interval structure, O(n) space in the number of outstanding out-of-order ranges.

Worked trace — window of segments 1..8 (each 1 byte for clarity), segments 3 and 6 lost:

Receiver gets 1,2 -> cumACK=3 (in order so far)
Gets 4 (hole at 3) -> cumACK=3, SACK [4,5)
Gets 5 -> cumACK=3, SACK [4,6)
Gets 7 (hole at 6) -> cumACK=3, SACK [7,8),[4,6)
Gets 8 -> cumACK=3, SACK [7,9),[4,6)

Sender scoreboard after these ACKs:
 3 = MISSING, 4-5 = SACKed, 6 = MISSING, 7-8 = SACKed
 -> Retransmit exactly {3, 6} in ONE round trip.

A cumulative-ACK sender would have needed two RTTs (recover 3, then discover 6). SACK recovers both losses in one.

Where it is used in real systems

SACK is ubiquitous and on by default in every mainstream stack:

  • Linux: net.ipv4.tcp_sack = 1 by default; the kernel keeps a per-connection scoreboard and RFC 6675 SACK-based recovery is the standard loss detector. Related sysctls: tcp_dsack, tcp_fack (forward-ACK, now largely superseded by RACK-TLP).
  • Windows, macOS, FreeBSD: all negotiate SACK-Permitted in the SYN and use SACK recovery.
  • RACK-TLP (RFC 8985): modern time-based loss detection consumes SACK information to decide which segments are lost by comparing transmit times, replacing the fixed 3-dupack rule.
  • QUIC (RFC 9000): not TCP, but its ACK frames are the philosophical successor — they carry many explicit ACK ranges, unconstrained by the 40-byte option limit, precisely because SACK proved the value of selective reporting.

Because SACK dramatically improves recovery on lossy and long-fat networks, it is a load-bearing part of CDN, video-streaming, and data-center TCP performance. It is one of the few TCP extensions whose adoption is essentially universal.

Comparison to the alternatives and the concrete tradeoff

SACK's closest cousin is NewReno (RFC 6582), the best you can do with cumulative ACKs alone. NewReno stays in fast recovery across a window, using each partial ACK to retransmit the next segment — but it can only chase one hole per RTT because it has no explicit map. SACK's advantage grows with the number of losses per window:

  • NewReno: ~N RTTs to recover N losses; zero extra header bytes.
  • SACK: ~1 RTT to recover up to a windowful of losses; costs up to 34 header bytes per ACK and requires scoreboard state.

Against go-back-N (retransmit everything from the first loss), SACK is strictly better on wasted bandwidth. Against a hypothetical full selective-repeat protocol, SACK is a pragmatic compromise: the 4-block limit means a receiver with more than 4 disjoint holes cannot report them all in one ACK, though it reports the newest data first so recovery still makes progress. D-SACK (RFC 2883) extends the scheme so the receiver can also report duplicate arrivals, letting the sender detect and back off from spurious retransmissions (e.g., from an underestimated RTO or packet reordering).

Pitfalls, failure modes, and why SACK still matters

SACK is robust but not free of sharp edges:

  • Reneging: SACK is a hint, not a contract. A memory-pressured receiver may drop SACKed out-of-order data; the sender must keep everything buffered until the cumulative ACK advances, so SACK does not reduce sender memory.
  • The 4-block ceiling: heavy loss or heavy reordering can create more holes than fit in one option, especially alongside Timestamps (3 blocks). Recovery still progresses but less optimally.
  • Middlebox interference: some firewalls/NATs strip or mangle TCP options, silently disabling SACK.
  • Security: the 2019 SACK Panic bugs (CVE-2019-11477/11478/11479) let a remote attacker send crafted SACKs that overflowed the Linux scoreboard's segment accounting, causing integer overflow and DoS — a reminder that parsing attacker-controlled block lists needs bounds checks.

Despite these, SACK is one of the highest-leverage TCP improvements ever standardized: nearly universal, backward compatible (falls back to cumulative ACK when unsupported), and the direct intellectual ancestor of QUIC's ACK ranges. It turned multi-loss recovery from an O(N)-RTT liability into a one-RTT operation.

SACK vs. other TCP loss-recovery schemes: how many losses each recovers per round trip and what the receiver reports.
SchemeReceiver reportsLosses recovered / RTTCost / limitation
Cumulative ACK (Tahoe/Reno)Highest in-order byte only1 loss per RTTMultiple losses in a window cause repeated timeouts / go-back-N-like stalls
NewReno (RFC 6582)Highest in-order byte + partial-ACK inferenceEffectively 1 per RTT (one hole at a time)No explicit gap map; infers holes from partial ACKs, slow on many losses
SACK (RFC 2018)Cumulative ACK + up to 4 received blocksMultiple (all reported holes) per RTTOnly 4 blocks fit in TCP option space; SACK info is advisory (may be reneged)
D-SACK (RFC 2883)SACK blocks + first block flags a duplicateSame as SACK + spurious-retransmit detectionRequires sender support to act on duplicate reports
QUIC ACK framesMany explicit ACK ranges (not option-limited)Many per RTTNew transport; not TCP, runs over UDP

Frequently asked questions

Is TCP SACK enabled by default?

Yes. Every mainstream stack — Linux, Windows, macOS, FreeBSD — negotiates SACK-Permitted in the SYN handshake and enables it whenever both endpoints agree. On Linux it is controlled by net.ipv4.tcp_sack, which defaults to 1. If either SYN omits the SACK-Permitted option, the connection silently falls back to cumulative-ACK recovery.

How many SACK blocks can a single ACK carry?

At most 4, because TCP options are capped at 40 bytes: the SACK option header is 2 bytes and each block (left + right 32-bit edges) is 8 bytes, giving floor((40-2)/8) = 4. When the TCP Timestamps option is also present it consumes 12 bytes, leaving room for only 3 SACK blocks. RFC 2018 requires the first block to describe the most recently received segment.

What is the difference between SACK and D-SACK?

SACK (RFC 2018) reports byte ranges the receiver has successfully received above the cumulative ACK, so the sender can retransmit only the gaps. D-SACK (RFC 2883) extends this so the receiver can also report ranges it received a second time (duplicates). That lets the sender detect spurious retransmissions caused by reordering or a too-aggressive RTO and adjust its behavior accordingly.

Does SACK let the sender free memory sooner?

No. SACK information is advisory: a receiver is allowed to renege and discard out-of-order data it previously SACKed. Only the cumulative acknowledgment is a firm promise. Therefore the sender must retain every unacknowledged segment until the cumulative ACK covers it, so SACK improves speed of recovery but not sender buffer occupancy.

How is SACK different from TCP NewReno?

NewReno is the best recovery achievable with cumulative ACKs alone; it uses partial ACKs to walk through holes one per round trip, with no explicit map of what arrived. SACK gives the sender an explicit list of received blocks, so it can retransmit multiple distinct losses in a single RTT. The tradeoff is header overhead (up to ~34 bytes/ACK) and scoreboard state that NewReno does not need.

What was the SACK Panic vulnerability?

SACK Panic (CVE-2019-11477 and related, 2019) was a set of Linux kernel bugs where crafted sequences of SACK segments could overflow the internal segment/scoreboard accounting (a 16-bit counter overflow), leading to a kernel panic or resource exhaustion — a remote denial of service. It was fixed by bounding how SACK blocks coalesce; it illustrates that parsing attacker-controlled block lists requires careful bounds checking.