Before a single byte of data moves
TCP is a connection-oriented protocol layered on top of IP, which is not — IP just forwards individual packets toward an address with no memory of what came before or after. TCP has to build the illusion of a reliable, ordered, connected stream on top of that memoryless best-effort substrate, and it starts by having both ends agree on where the stream begins. That agreement is the three-way handshake, and this simulation animates it packet by packet before moving into the part most explanations skip: what happens to the connection's throughput once data is actually flowing and the network hits congestion.
SYN, SYN-ACK, ACK
Each side of a TCP connection tracks its own sequence number — a counter identifying the byte offset of the stream it is about to send — and the handshake's entire job is exchanging and confirming these starting values:
client -> server: SYN, seq = x (I plan to start at byte x)
server -> client: SYN-ACK, seq = y, ack = x + 1 (I plan to start at y;
I confirm your x)
client -> server: ACK, ack = y + 1 (I confirm your y)
After this exchange both sides know the other's starting sequence number and have acknowledged it, which is what lets every subsequent data segment be tracked, placed in order and confirmed as received even though IP might deliver the underlying packets out of order or drop some entirely. The handshake also negotiates connection parameters — window size, maximum segment size, selective-acknowledgment support — piggybacked on the same three packets, which is why a slow or lossy handshake delays not just the connection start but the negotiated capabilities that shape everything after it.
Once connected: the congestion window
A live TCP connection cannot just blast data as fast as the sender's link allows — somewhere along the path there may be a slower link or a congested router, and sending faster than the network can carry just fills buffers until they overflow and packets are dropped. TCP's answer is the congestion window (cwnd): a sender-side limit on how many unacknowledged bytes may be in flight at once, adjusted continuously based on whether packets are getting through or being lost — this is AIMD, additive-increase multiplicative-decrease, and the sawtooth it produces is the second half of the live demo.
Slow start: exponential, despite the name
Immediately after the handshake, TCP has no information about the path's capacity, so it starts conservatively — but ramps up aggressively. Slow start doubles cwnd roughly every round-trip time, because every acknowledged segment increases cwnd by one segment's worth, and within one RTT an entire window's worth of segments gets acknowledged:
slow start (while cwnd < ssthresh): on each ACK received: cwnd += 1 segment // an entire cwnd of segments ACKed within ~1 RTT => cwnd roughly doubles per RTT // this is exponential growth despite the name "slow start" - // it's "slow" only relative to sending the whole window at once
This continues until cwnd reaches a threshold (ssthresh) — usually set from a previous loss event — or until a loss is actually detected, at which point TCP switches modes.
Congestion avoidance: additive increase
Once past ssthresh, TCP assumes it is close to the path's actual capacity and switches to cautious linear growth — increasing cwnd by roughly one segment per round-trip time rather than per ACK, probing gently for any remaining spare capacity:
congestion avoidance (cwnd >= ssthresh): on each full window of ACKs (~1 RTT): cwnd += 1 segment // linear growth - the "additive increase" half of AIMD
Loss: multiplicative decrease
A dropped packet (detected either by a retransmission timeout, or more commonly by duplicate ACKs signalling a gap in the received stream) is TCP's implicit signal that it has overrun the network's actual capacity — there is no explicit congestion notification, TCP infers congestion purely from loss. The response is an aggressive multiplicative cut, not a gentle correction:
on loss detected:
ssthresh = cwnd / 2
cwnd = ssthresh (or cwnd = 1 segment, for a timeout -
a full restart back to slow start)
-> resume in congestion avoidance from the halved value
The asymmetry — additive increase, multiplicative decrease — is deliberate and is what makes AIMD converge fairly and stably even when many independent TCP connections share one bottleneck link with no coordination between them: growing linearly is cautious enough not to overshoot capacity by much before the next loss signal arrives, and halving on loss backs off fast enough to drain a congested queue quickly rather than repeatedly overshooting it. The repeated cycle of linear climb followed by a halving on loss produces the characteristic sawtooth throughput graph that is the second half of this simulation's live plot, and it is one of the most consequential pieces of algorithm design on the entire internet — it is the reason a congested link degrades gracefully into shared, fair bandwidth instead of collapsing into gridlock.
Frequently asked questions
Why does TCP need three packets to start a connection instead of one?
Because both directions need an agreed starting sequence number, and each side needs proof the other actually received its number, not just sent it. SYN proposes the client's starting sequence, SYN-ACK proposes the server's and confirms the client's, and the final ACK confirms the server's — after three packets both sides have a mutually confirmed, not just mutually claimed, starting point.
If slow start grows the window exponentially, why is it called 'slow'?
It's slow only relative to the alternative of sending an entire large window's worth of data immediately with no ramp-up at all, which is what earlier, more naive TCP implementations did and which reliably overwhelmed intermediate routers. Compared to that, starting from one segment and doubling each round-trip is the conservative option — even though in absolute terms the growth is exponential.
Why does TCP cut its sending rate in half on packet loss instead of a smaller correction?
Because TCP has no explicit signal for how congested the network is — a lost packet is the only feedback it gets, and by the time loss is detected, the actual congestion may already be significant. A large, fast cut (multiplicative decrease) drains a congested queue quickly; the subsequent slow, linear climb back up (additive increase) is what makes the algorithm converge to a stable, fair share of bandwidth without repeatedly overshooting and re-triggering loss.
Try it live
Everything above runs in your browser — open TCP/IP Handshake and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open TCP/IP Handshake simulation