Thuta Learning
IntermediateDevOps & Toolsbeginner

TCP Reliability: ACKs, Retransmission, and Windows

What you'll walk away with

  • Explain the core ideas behind TCP Reliability: ACKs, Retransmission, and Windows
  • Read the diagram and trace how data actually moves through it
  • Run the sample code and verify its output

Build the mental model

One fact explains most of TCP's behaviour: sequence numbers count bytes, not packets. A segment's sequence number is the position in the stream of its first byte. That is why TCP can freely re-segment data — a retransmission need not match the original segment boundaries. Acknowledgements are cumulative: an ACK names the next byte the receiver expects, so a hole freezes the ACK no matter how much arrives after it. That is robust but throws away information, which is why SACK was added later to report which blocks beyond the hole arrived.

When no ACK comes back, the sender's retransmission timer expires and it sends the data again. That timeout is not a constant; it is derived from the sender's running estimate of round-trip time and its variance, so a satellite link and a LAN get very different values. Waiting for the RTO is slow, so TCP also watches for three duplicate ACKs in a row and retransmits immediately — fast retransmit.

Sending one segment and waiting would limit you to one per round trip, hence the sliding window: the bytes allowed in flight unacknowledged. There are two windows, and they are constantly confused. The receive window (rwnd) is advertised by the receiver in every segment and means 'this much buffer space is left' — that is flow control. The congestion window (cwnd) is the sender's estimate of what the network path can absorb, learned by watching for loss — that is congestion control. You may send the minimum of the two. A slow receiver and a congested network are different failures, so TCP keeps separate machinery for each.

text
SENDER'S SLIDING WINDOW OVER THE BYTE STREAM
--------------------------------------------
   +----+----+----+----+----+----+----+----+----+----+----+----+
   | 00 | 04 | 08 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 |
   +----+----+----+----+----+----+----+----+----+----+----+----+
   |<--- ACKed -->|<-- sent, not ACKed --->|<-- cannot send -->|
                  ^                        ^
            send_base                 send_base + rwnd

ACK for 1024 arrives -> send_base moves right, the window slides
ACK for 1012 repeats -> hole at 1012, window frozen until the
                        retransmission fills it

Connect it to a real scenario

Say you are copying a 1 GB backup from a server in Singapore to one in Yangon. Both ends have gigabit links, yet the transfer sits at about 6 Mbps. This is not a bandwidth problem, it is a window problem. If the round-trip time is 80 ms and the receive window is 64 KB (the maximum TCP can express without the window scaling option), then you can only have 64 KB in flight per round trip, so the ceiling is 64 KB / 0.08 s, roughly 800 KB/s. No amount of extra bandwidth moves that number. This quantity is the bandwidth-delay product, and the fix is to make sure window scaling is enabled — it is on by default in modern operating systems, but old middleboxes have been known to strip the option.

Watch for the opposite symptom too. If ACKs keep flowing but throughput periodically collapses and you see a little packet loss, that is not a slow receiver, it is congestion control doing its job: cwnd is cut on every loss and grows back, producing the familiar sawtooth. The diagnostic is straightforward. A small advertised rwnd points at one of the two endpoints; loss with a large rwnd points at the network in between.

Try the working example

python
ISN = 1000      # sequence number of the first byte of the stream
MSS = 4         # bytes carried per segment
RWND = 12       # receiver's advertised window, in bytes
TOTAL = 24      # bytes the application handed to TCP

# The network drops segment 3 the first time it is transmitted.
DROP_FIRST_TRY = {3}

send_base = ISN     # oldest byte sent but not yet acknowledged
next_seq = ISN      # next byte we are allowed to put on the wire
received = set()    # segment numbers the receiver actually holds
already_dropped = set()
on_wire = 0


def cumulative_ack(received_segs):
    """The ACK number is the next byte expected IN ORDER, so a hole
    in the middle freezes it no matter what arrives after the hole."""
    n = 0
    while n in received_segs:
        n += 1
    return ISN + n * MSS


print("seg   seq  event                      ACK  base  usable")
print("---  ----  -------------------------  ----  ----  ------")

while send_base < ISN + TOTAL:
    progressed = False

    # 1. Fill the window: send every segment the window still allows.
    while next_seq < send_base + RWND and next_seq < ISN + TOTAL:
        seg = (next_seq - ISN) // MSS
        if seg in DROP_FIRST_TRY and seg not in already_dropped:
            already_dropped.add(seg)
            event = "sent -> DROPPED en route"
        else:
            received.add(seg)
            event = "sent -> arrives"
        next_seq += MSS
        on_wire += 1
        ack = cumulative_ack(received)
        print("%3d  %4d  %-25s  %4d  %4d  %6d"
              % (seg, next_seq - MSS, event, ack, send_base,
                 send_base + RWND - next_seq))

    # 2. Slide the window as far as the cumulative ACK allows.
    ack = cumulative_ack(received)
    if ack > send_base:
        send_base = ack
        progressed = True

    # 3. Window full and the ACK never moved -> the RTO fires and we
    #    retransmit only the segment sitting at send_base.
    if not progressed:
        seg = (send_base - ISN) // MSS
        received.add(seg)
        on_wire += 1
        ack = cumulative_ack(received)
        print("%3d  %4d  %-25s  %4d  %4d  %6d"
              % (seg, send_base, "RETRANSMIT after timeout", ack,
                 send_base, send_base + RWND - next_seq))
        send_base = ack

print()
print("bytes delivered in order :", cumulative_ack(received) - ISN)
print("segments put on the wire :", on_wire, "for", TOTAL // MSS, "segments")
You should see
seg   seq  event                      ACK  base  usable
---  ----  -------------------------  ----  ----  ------
  0  1000  sent -> arrives            1004  1000       8
  1  1004  sent -> arrives            1008  1000       4
  2  1008  sent -> arrives            1012  1000       0
  3  1012  sent -> DROPPED en route   1012  1012       8
  4  1016  sent -> arrives            1012  1012       4
  5  1020  sent -> arrives            1012  1012       0
  3  1012  RETRANSMIT after timeout   1024  1012       0

bytes delivered in order : 24
segments put on the wire : 7 for 6 segments

5-minute try-it

Change RWND from 8 to 24 and rerun: how does the number of segments on the wire change, and why does a bigger window not always help? Then set DROP_FIRST_TRY to {1, 4} and watch how a cumulative ACK behaves with two holes instead of one.

One important caution

Assuming slow throughput means insufficient bandwidth. On a high-RTT link a small window caps you regardless of how much bandwidth you buy.

Treating flow control and congestion control as one thing. Tuning rwnd does nothing for congestion, and switching congestion algorithms does nothing for a receiver whose buffer is too small.

RFC 5681 - TCP Congestion ControlComputer Networking

Easy traps

  • Assuming slow throughput means insufficient bandwidth. On a high-RTT link a small window caps you regardless of how much bandwidth you buy.
  • Treating flow control and congestion control as one thing. Tuning rwnd does nothing for congestion, and switching congestion algorithms does nothing for a receiver whose buffer is too small.
  • Validate sample code in a local or test environment before applying it to a production network.

Exercise

Change RWND from 8 to 24 and rerun: how does the number of segments on the wire change, and why does a bigger window not always help? Then set DROP_FIRST_TRY to {1, 4} and watch how a cumulative ACK behaves with two holes instead of one.

You'll know it worked when: seg seq event ACK base usable --- ---- ------------------------- ---- ---- ------ 0 1000 sent -> arrives 1004 1000 8 1 1004 sent -> arrives 1008 1000 4 2 1008 sent -> arrives 1012 1000 0 3 1012 sent -> DROPPED en route 1012 1012 8 4 1016 sent -> arrives 1012 1012 4 5 1020 sent -> arrives 1012 1012 0 3 1012 RETRANSMIT after timeout 1024 1012 0 bytes delivered in order : 24 segments put on the wire : 7 for 6 segments

TCP Reliability: ACKs, Retransmission, and Windows | Thuta Learning