TCPsequence numberacknowledgmentcumulative ACKSACKreassembly

Sequence Numbers, ACKs and Reassembly

Every byte in a TCP stream has a number; a segment carries the number of its first byte, an ACK carries the number of the next byte the receiver wants, and out-of-order segments wait in a reassembly buffer until the hole before them is filled — which is why numbers are byte offsets, why cumulative ACKs cannot describe a gap, and why SACK exists.

ConceptualLinux
▶ InteractiveInterview question
Progress

The problem

Three segments carry bytes 0–999, 1000–1999 and 2000–2999. The middle one is lost and the third arrives first. The receiver must not deliver bytes 2000–2999 to the application yet, must tell the sender exactly what is missing, and must do it without either side keeping a list of every packet ever sent.

Bytes on a number line

TCP numbers bytes, not segments. The connection’s ISN (The Three-Way Handshake) is byte zero’s number minus one; every byte written afterwards gets the next number. A segment’s sequence number is the number of its first byte; its length tells the receiver which range it covers. In the relative numbering that Wireshark shows and this lesson uses, the first segment carries bytes 0–999, the second 1000–1999, the third 2000–2999.

An acknowledgment number names the *next byte the receiver expects* — one past the highest byte received in order. After segment one, the receiver ACKs 1000. That single number says "I have everything below 1000, contiguous, no gaps". ACKs are cumulative: an ACK of 3000 acknowledges everything below it, including earlier segments whose own ACKs were lost, so lost ACKs cost nothing as long as a later one gets through.

Three segments, all delivered in order
sender                                  receiver
seg 1  seq=0     len=1000  [0..999]   ->   in order; deliverable
                                      <-   ACK 1000   "next I want is 1000"
seg 2  seq=1000  len=1000  [1000..1999] -> in order; deliverable
                                      <-   ACK 2000
seg 3  seq=2000  len=1000  [2000..2999] -> in order; deliverable
                                      <-   ACK 3000
(receivers usually ACK every second segment: "delayed ACK", up to ~40 ms on Linux)

Cumulative ACKs and their blind spot

Now lose segment two. Segment three arrives; the receiver stores bytes 2000–2999 but cannot deliver them, because 1000–1999 is missing and the stream must be in order. What does it ACK? The only number a cumulative ACK can carry is 1000 — the next byte it needs — the same number it already sent. The sender sees a duplicate ACK. Segment four arrives (3000–3999): another ACK 1000. The sender now knows *something* after byte 999 is missing and that *some* later data arrived, but not which; it cannot tell "lost segment two only" from "lost two, three and four".

The classic sender behaviour (Reno) retransmits from the ACK number and then waits to see what the next ACK says: if it jumps to 5000, everything was there; if it moves to 2000, another hole. Each hole costs a round trip to discover. On a path with 150 ms RTT and a burst of loss, that is a very slow way to recover, and it is the whole reason the next mechanism exists.

Segment two lost, three and four arrive
seg 1  [0..999]      ->   ACK 1000
seg 2  [1000..1999]  -x   (lost)
seg 3  [2000..2999]  ->   held out of order;   ACK 1000   (dup #1)
seg 4  [3000..3999]  ->   held out of order;   ACK 1000   (dup #2)
seg 5  [4000..4999]  ->   held out of order;   ACK 1000   (dup #3)  -> fast retransmit of seg 2
seg 2' [1000..1999]  ->   hole filled; 1000..4999 now in order;   ACK 5000

SACK: telling the sender what did arrive

Selective acknowledgment (RFC 2018) adds an option to the ACK listing up to three or four ranges the receiver holds beyond the cumulative point: ACK 1000, SACK 2000–3000, 3000–4000 (the ranges merge into 2000–4000 as they become contiguous). The sender now knows precisely that bytes 1000–1999 are the only hole and retransmits exactly those, once. With several holes it retransmits each of them in one round trip instead of one per round trip. Both sides must have agreed SACK in the handshake; every mainstream stack has it on by default, and a SYN with the option stripped is a measurable performance loss.

The sender keeps a scoreboard: for every byte in flight, has it been SACKed, is it presumed lost, has it been retransmitted. Modern loss detection (RACK on Linux) uses the scoreboard plus timing — a segment is presumed lost if a segment sent *later* has been SACKed and enough time has passed — which catches losses that never generate three duplicate ACKs, such as the last segments of a transfer. Packet Loss: Duplicate ACKs, Fast Retransmit and the RTO covers the timers.

  • Cumulative ACK: one number, "everything below this".
  • SACK blocks: "and also these ranges"; merged as they touch.
  • Sender scoreboard: what is SACKed, lost, retransmitted — the input to every retransmission decision.

The receiver’s reassembly buffer

Linux

Out-of-order segments are not discarded; they are held in an out-of-order queue attached to the socket until the hole before them fills. The moment the retransmission of 1000–1999 arrives, the receiver splices it in, finds 1000–4999 contiguous, and moves all of it to the in-order receive buffer in one step; a single read() can then return 4000 bytes. The application sees a stall of one round trip (or an RTO) followed by a burst — never an error, never a gap. That stall is Head-of-Line Blocking in its purest form.

Held segments occupy receive-buffer memory and count against the window advertised to the sender (Flow Control: The Receive Window); a window full of out-of-order data with one missing segment is a stalled connection. Linux exposes the queue in ss -ti as ofo counters and in nstat as TcpExtTCPOFOQueue; a high rate on a link that should not lose packets often means reordering — ECMP hashing gone wrong, link aggregation, or a bonding driver — which TCP treats as loss unless the reordering detection (tcp_reordering, RACK) absorbs it.

Why byte offsets and not segment counts

If TCP numbered segments, a retransmission would have to be the same segment — same boundaries, same size. Numbering bytes frees the sender to re-segment: retransmit 1000–1999 as two 500-byte pieces if the path MTU shrank, coalesce 1000–2999 into one segment if the MSS allows, or let the NIC do segmentation offload (TSO/GSO) on a 64 kB chunk and split it any way it likes. The receiver does not care how the bytes were packaged; it only cares which byte numbers arrived. Any two segments with overlapping ranges are simply merged.

Byte numbering also makes the ACK a byte count, which is exactly what the sender’s buffer management needs: "ACK 5000" means bytes below 5000 can be freed from the send buffer, whatever segments they travelled in. The only cost is the 32-bit space: at 10 Gbit/s the numbers wrap every ~3.4 seconds (4 GiB), and an old segment from before the wrap could be mistaken for a new one — so the timestamp option (PAWS, RFC 7323) tags every segment with a monotonically increasing clock and the receiver discards anything from the past.

Key points

  • A sequence number is the offset of a segment’s first byte; an ACK is the next byte the receiver expects.
  • ACKs are cumulative: one number covers everything below it, so lost ACKs are harmless.
  • A hole makes the receiver repeat the same ACK — duplicate ACKs — which tells the sender that something is missing but not what.
  • SACK lists the ranges held beyond the hole so the sender retransmits exactly the gap, in one round trip.
  • Out-of-order data waits in a reassembly queue, counts against the window, and is delivered in one burst when the hole fills.
  • Numbering bytes lets segments be split, merged and offloaded freely; the 32-bit space wraps quickly at high speed, so timestamps disambiguate.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why cumulative ACKs at all, if they cannot describe gaps?

Robustness and size: one 32-bit number, tolerant of any lost ACK, no state about individual packets. SACK is layered on top for the cases where the gap description is worth the extra bytes.

Why hold out-of-order data instead of dropping it?

Dropping it would force the sender to resend everything after the hole — go-back-N — and multiply the cost of one lost packet by the window size. Holding it means one loss costs one retransmission.

Why does the receiver delay ACKs?

To halve the ACK traffic and to give the application a chance to reply so the ACK can piggyback on data. The cost is up to 40 ms of latency on the last segment of a request, which is why interactive protocols set TCP_NODELAY and why Linux quick-ACKs during slow start.

A byte stream, segment by segment

A 4,000-byte stream, segment by segment
TCP numbers bytes, not packets. The ACK carries the next byte the receiver wants, which is why a single missing segment stalls the acknowledgement.
Receiver buffer (4,000 bytes)ack=0
0–999not yet
1000–1999not yet
2000–2999not yet
3000–3999not yet
send seq=0 len=1000  (bytes 0–999)
Why byte offsets: the network may split or merge segments (path MTU, coalescing), so numbering packets would be meaningless. Numbering bytes lets the receiver reassemble any mix of segment sizes into one exact stream and name precisely what is missing.
1/8Educational model

How it fails

What the failure looks like from inside real software.

  • Reordering on an ECMP or bonded path: the receiver’s out-of-order queue is busy, the sender sees duplicate ACKs, fast-retransmits data that was never lost, and halves its window — throughput collapses on a link with zero real loss.
  • A middlebox stripping SACK from the handshake: every loss event costs one round trip per hole; a lossy Wi-Fi link that should recover in 50 ms takes seconds.
  • One hole and a full window of out-of-order data behind it: the connection is stalled, the receive buffer is full of undeliverable bytes, and the application’s read() sits idle while ss shows a large Recv-Q.
  • A 10 Gbit/s transfer with timestamps disabled: sequence wrap ambiguities cause mysterious data corruption or resets under reordering; PAWS exists for this.
  • Delayed ACK meeting Nagle’s algorithm in a request/response protocol with small writes: a 40 ms stall on every exchange, fixed by TCP_NODELAY or by writing each message in one call.