TCP in Depth: Flow Control, Congestion Control and Loss
How TCP keeps data ordered and the network healthy: windows, slow start, retransmission, head-of-line blocking and TIME_WAIT.
- 10 segmentsModern initial congestion window
- ~15 Mbit/s64 segments in flight at 50 ms
- ~470 per secondNew connections one client can open: TIME_WAIT limit
Show what to say
“TCP gives an ordered reliable stream with sequence numbers, ACKs and retransmission. Flow control protects the receiver with its advertised window; congestion control protects the network with slow start and then add-one, halve-on-loss. Three duplicate ACKs trigger a fast retransmit, a timeout resets the window to one, and one lost segment blocks everything behind it.”
Trade-offs
- Fast retransmit vs timeout: duplicate ACKs recover in about a round trip; a timeout idles for the whole RTO and restarts from one segment.
- Reno-style loss reaction vs BBR: reacting to loss is simple and fair but fills buffers; BBR models bandwidth and delay and can be unfair.
- Closing first vs second: the side that closes first holds TIME_WAIT, so decide which end can afford the sockets.
Bring it up before they ask
Do not tune TIME_WAIT away; reuse connections with keep-alive or a pool, because port exhaustion is a symptom of too many short connections.
turns lossy packets into an ordered, reliable stream, with to protect the receiver and to protect the network. A loss costs more than one packet: it shrinks the window and stalls everything behind it.
What TCP promises
Data arrives once and in order, or the connection fails.
Read more · 93 words
The sender numbers every byte, the receiver acknowledges the next byte it expects (a cumulative ), and unacknowledged data is resent.
- Client
- Service
Text description
- Sender → Receiver: segment 1
- Receiver replies to Sender: ACK 2 (next expected)
- Sender → Receiver: segment 2 (lost)
- Sender → Receiver: segments 3, 4, 5
- Receiver replies to Sender: ACK 2, ACK 2, ACK 2 (duplicates)
- Sender → Receiver: segment 2 again
- Receiver replies to Sender: ACK 6
Quick checkWhat does TCP guarantee about data?
Flow control versus congestion control
Read more · 50 words
| Flow control | Congestion control | |
|---|---|---|
| Protects | The receiver's buffer | The network between you |
| Limit | Receive window (rwnd), sent in each ACK | Congestion window (cwnd), computed by the sender |
| Signal | The receiver says how much room it has | Loss or delay |
The sender may have at most min(rwnd, cwnd) bytes unacknowledged.
Quick checkFlow control protects which side?
Congestion control: slow start and AIMD
A new connection probes the network.
Read more · 97 words
In slow start the window doubles every round trip from 10 segments. Past the threshold (ssthresh) it adds one per round trip, and on loss it cuts the window. This add-one, halve-on-loss rule (AIMD) lets many flows share a link fairly.
Try each loss event. is roughly one window per round trip, so 64 segments at 50 ms is about 15 Mbit/s.
Linux defaults to CUBIC; BBR models bandwidth and delay instead of reacting to loss.
// The congestion window (cwnd), in segments, for each round trip.
// Slow start doubles it until ssthresh, then congestion avoidance adds 1 per round trip.
// A loss cuts it: 3 duplicate ACKs halve it and carry on; a timeout drops it to 1 and starts over.
export function cwndSeries({ rounds = 30, initial = 10, ssthresh = 64, loss = null } = {}) {
const out = [];
let cwnd = initial;
let threshold = ssthresh;
for (let r = 0; r < rounds; r++) {
out.push(cwnd);
if (loss && r === loss.round) {
threshold = Math.max(2, Math.floor(cwnd / 2));
cwnd = loss.kind === 'timeout' ? 1 : threshold;
} else if (cwnd < threshold) {
cwnd = Math.min(cwnd * 2, threshold);
} else {
cwnd += 1;
}
}
return out;
}
// Throughput is roughly one window per round trip.
export const throughputMbps = (segments, rttMs = 50, mss = 1460) => (segments * mss * 8) / (rttMs / 1000) / 1e6;Quick checkIn slow start, how does the window grow each round trip?
Loss and retransmission
After three duplicate the sender retransmits at once (fast retransmit).
Read more · 59 words
If nothing arrives, it waits for the retransmission timeout ().
The RTO is learned: the smoothed round-trip time plus four times its variation.
A timeout is the expensive case: the sender idles for the whole RTO, then restarts from one segment.
// How long should the sender wait for an ACK before retransmitting? (RFC 6298)
// Track a smoothed round-trip time and how much it varies; wait the smoothed time plus 4 variations.
export function makeRto({ minMs = 200 } = {}) {
let srtt = null;
let rttvar = 0;
return function sample(rttMs) {
if (srtt === null) {
srtt = rttMs;
rttvar = rttMs / 2;
} else {
rttvar = 0.75 * rttvar + 0.25 * Math.abs(srtt - rttMs);
srtt = 0.875 * srtt + 0.125 * rttMs;
}
return { srtt, rttvar, rto: Math.max(minMs, srtt + 4 * rttvar) };
};
}Quick checkThree duplicate ACKs arrive. What does the sender do?
Head-of-line blocking
If segment 2 is lost, segments 3 to 5 may already have arrived, but the application sees none of them until segment 2 is resent.
Read more · 91 words
That wait is . It is why one loss stalls every stream, and why moved to QUIC, which orders each stream separately.
Quick checkOne lost segment stalls data the app already received. What is this?
Closing a connection and TIME-WAIT
Closing takes a FIN and an in each direction.
Read more · 167 words
The side that closes first then waits in (60 seconds on Linux, twice the maximum segment lifetime). That ensures its final ACK arrives and that late packets from the old connection cannot be mistaken for a new one.
Each TIME-WAIT holds a source port. With about 28,000 ephemeral ports and 60 seconds, one client can open only about 470 new connections a second to one destination.
// After closing, the side that closed first keeps the connection in TIME_WAIT (60 s on Linux)
// so late packets cannot leak into a new connection. Each TIME_WAIT holds one source port.
// To one destination ip:port, a client can therefore open only this many new connections per second:
export const maxNewPerSecond = (ephemeralPorts = 28232, timeWaitSeconds = 60) => ephemeralPorts / timeWaitSeconds;Quick checkWhich side ends up in TIME_WAIT?
Other knobs worth knowing
Read more · 174 words
- Nagle's algorithm and delayed . Together they can add 40 ms stalls to small writes. Latency-sensitive services set
TCP_NODELAY. - Window scaling. The window field is 16 bits, so a negotiated scale factor lets it reach gigabytes for fast long-distance links.
- . Idle connections die silently behind NATs and firewalls; keepalive or application detect it.
- SYN flood and backlog. A full accept queue drops new connections. SYN cookies defend against floods.
- MSS and MTU. Segments are sized to fit one packet (about 1460 bytes on Ethernet), so large writes are split.
Follow-up questions
What is the difference between flow control and congestion control?
protects the receiver with the window it advertises. protects the network with a window the sender computes from loss. The sender obeys the smaller.
Explain slow start and what happens on packet loss.
The window doubles each round trip until ssthresh, then grows by one. Three duplicate halve it; a timeout resets it to one segment.
What is TIME-WAIT and why can it cause trouble?
The side that closes first holds the connection about 60 seconds. Many short connections exhaust ports; reuse connections.
Check yourself
Question 1 of 7Score 2 out of 3 or better to mark this page complete.