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.
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 itConnect 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
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")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 segments5-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 Control — Computer Networking