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.
The problem
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.
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.
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
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
send seq=0 len=1000 (bytes 0–999)
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 whilessshows 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_NODELAYor by writing each message in one call.