Loading content...
There's an ancient legend about the inventor of chess presenting his game to a king. When offered any reward, the inventor asked for something seemingly modest: one grain of rice on the first square, two on the second, four on the third, and so on—doubling for each of the 64 squares.
The king initially laughed at this modest request. By the end of the calculation, he owed 18,446,744,073,709,551,615 grains—far exceeding all the rice in history.
This same mathematical principle powers TCP slow start. The exponential growth of the congestion window isn't just a convenient choice—it's a precisely calibrated mechanism that balances aggressive capacity discovery against catastrophic network overload. Understanding exponential growth deeply reveals why slow start works as well as it does.
This page dissects the mathematics, visualizes the behavior, analyzes the theoretical foundations, and explores how exponential growth manifests in real networks. By the end, you'll understand why doubling is the optimal growth rate for network probing.
This page covers the mathematical foundations of exponential growth in slow start, why doubling per RTT minimizes wasted transmissions, how growth appears in packet traces and graphs, and the relationship between growth rate and network efficiency. You'll gain the analytical tools to reason about slow start behavior in any scenario.
Before analyzing slow start's growth, let's establish the mathematical framework:
The basic recurrence:
Let cwnd(n) represent the congestion window after n round-trip times. During slow start:
cwnd(n) = cwnd(n-1) × 2
Within each RTT, the sender transmits cwnd segments and receives cwnd acknowledgments. Since each ACK increases cwnd by 1 MSS, the total increase per RTT equals the window size itself—hence the doubling.
The closed-form solution:
Solving this recurrence relation yields:
cwnd(n) = cwnd(0) × 2^n
Where cwnd(0) is the initial window (typically 10 MSS in modern implementations).
Growth rate analysis:
The growth factor per RTT is exactly 2 (or 100% increase). Expressed logarithmically:
log₂(cwnd(n)) = log₂(cwnd(0)) + n
This means the number of RTTs required to reach any target window size T is:
n = log₂(T / cwnd(0)) = log₂(T) - log₂(cwnd(0))
| RTT # | cwnd (segments) | cwnd (bytes) | Cumulative Data Sent | Throughput if RTT = 50ms |
|---|---|---|---|---|
| 0 | 10 | 14,600 | 14.6 KB | 2.3 Mbps |
| 1 | 20 | 29,200 | 43.8 KB | 4.7 Mbps |
| 2 | 40 | 58,400 | 102.2 KB | 9.4 Mbps |
| 3 | 80 | 116,800 | 219.0 KB | 18.7 Mbps |
| 4 | 160 | 233,600 | 452.6 KB | 37.4 Mbps |
| 5 | 320 | 467,200 | 919.8 KB | 74.8 Mbps |
| 6 | 640 | 934,400 | 1,854.2 KB | 149.5 Mbps |
| 7 | 1,280 | 1,868,800 | 3,722.6 KB | 299.0 Mbps |
| 8 | 2,560 | 3,737,600 | 7,460.2 KB | 598.0 Mbps |
| 9 | 5,120 | 7,475,200 | 14,935.4 KB | 1.2 Gbps |
| 10 | 10,240 | 14,950,400 | 29,885.4 KB | 2.4 Gbps |
Since cwnd doubles every RTT, we can apply the 'Rule of 72' variant for prediction. To reach N times the initial capacity, you need approximately log₂(N) RTTs. Want to reach 1000× initial speed? That's about 10 RTTs (2¹⁰ = 1024). This logarithmic relationship is why slow start efficiently handles the enormous range of network capacities.
The choice to double cwnd per RTT isn't arbitrary. It represents a multiplicative probe that minimizes worst-case wasted bandwidth while maintaining rapid discovery.
The multiplicative probe principle:
Imagine you're searching for an unknown capacity C somewhere between 1 and 1,000,000 segments. You probe by sending increasingly larger bursts until you find C (through packet loss).
With any multiplicative factor k > 1:
The waste analysis:
The "waste" occurs in the final probe—you send up to k×C data but only C can get through. The excess (k-1)C is lost to congestion.
Waste fraction = (k-1)C / (total probed) = (k-1)C / C = (k-1)
For k=2 (doubling), maximum waste is 50%. For k=3 (tripling), maximum waste is 67%. For k=1.5, maximum waste is 33% but requires more probes.
The k=2 equilibrium:
Doubling strikes a balance optimized for real network conditions:
Time-to-capacity: With k=2, reaching 10 Gbps from 14 KB takes ~20 RTTs. At 50ms RTT, that's 1 second. Tripling saves only ~7 RTTs (0.35 seconds) but increases waste by 50%.
Buffer sizing: Network buffers are typically sized to hold about one bandwidth-delay product. Doubling means exceeding by 1× BDP, which stressed buffers can usually absorb. Tripling means exceeding by 2× BDP, which may cause multiple drops.
Fairness: Multiple connections entering slow start simultaneously will interleave their probes. Doubling provides predictable behavior that allows fair sharing as flows complete slow start at similar rates.
Mathematical proof of optimality:
For a given number of probes n and starting value W₀:
Minimizing total RTTs while bounding waste to 50% requires k ≤ 2. Maximizing k (speed) subject to this constraint gives k = 2.
This is why Van Jacobson's original choice of doubling has proven robust for 35+ years.
Some experimental congestion control algorithms have tried different growth rates. For instance, some propose 'additive increase during slow start' after initial RTTs, or rate-based approaches that don't use discrete doubling at all. BBR, for example, probes at 2× estimated bandwidth for a period rather than per-RTT doubling. These alternatives work in specific environments but haven't displaced the simple elegance of doubling for general-purpose TCP.
Understanding how exponential growth appears visually helps diagnose real network behavior. Let's examine several perspectives:
Linear-scale graphs:
When plotting cwnd vs time on a linear scale, slow start produces the characteristic "hockey stick" shape:
Logarithmic-scale graphs:
Plotting cwnd on a log scale reveals the true nature:
Packet-level visualization:
Viewing individual packets on a time-sequence plot shows:
| Tool | What to Look For | Healthy Pattern | Problem Indicator |
|---|---|---|---|
| Wireshark I/O Graph | Bytes/sec over time | Smooth exponential rise until plateau | Flat or sawtooth pattern early |
| tcptrace RTT Plot | Round-trip time samples | Stable or slowly increasing RTT | Sudden RTT spikes during ramp |
| ss -i (Linux) | cwnd value in output | cwnd matching expected growth | cwnd stuck at low value |
| Throughput Graph | Bandwidth utilization | Steep rise then steady state | Never reaches expected speed |
| Sequence Diagram | Segment timing | Doubling bursts per RTT | Irregular burst sizes |
Expected cwnd trajectory example:
For a 100 Mbps path with 50ms RTT (BDP = 625KB ≈ 428 segments at 1460 MSS):
Time (ms) cwnd State
───────────────────────────────
0 10 Slow start begins
50 20 First ACKs received
100 40 Doubling continues
150 80 Approaching capacity
200 160 Still doubling
250 320 Almost at BDP
300 640 Exceeds BDP!
300+ ~428 Loss occurs, cwnd adjusts
Notice that cwnd exceeds the BDP (428 segments) at RTT 6. At this point, the network cannot forward packets as fast as they arrive, queues build, and eventually loss occurs. This is the expected termination of slow start—the protocol successfully discovered the path capacity.
On log-scale plots, slow start is a straight line. On linear plots, it's an exponential curve. If you see slow start appearing linear on a linear plot, the time scale is probably too compressed—zoom in to see the true exponential shape. This visual intuition helps quickly categorize network behavior during troubleshooting.
The effective speed of slow start's exponential growth depends on network parameters. Let's analyze how RTT and bandwidth affect the ramp-up:
RTT determines wall-clock growth speed:
Since cwnd doubles per RTT, the actual time to reach a target capacity is:
Time to capacity = RTT × log₂(Target / Initial)
For the same target:
This has profound implications for high-latency networks.
| Network Type | Typical RTT | Capacity | BDP | RTTs to BDP | Wall-Clock Time |
|---|---|---|---|---|---|
| Data center | 0.5 ms | 10 Gbps | 625 KB | 6 | 3 ms |
| Local metro | 5 ms | 1 Gbps | 625 KB | 6 | 30 ms |
| Cross-country US | 50 ms | 100 Mbps | 625 KB | 6 | 300 ms |
| Transatlantic | 100 ms | 100 Mbps | 1.25 MB | 7 | 700 ms |
| Global CDN | 150 ms | 50 Mbps | 937 KB | 6-7 | 1000 ms |
| Geostationary satellite | 600 ms | 10 Mbps | 750 KB | 6 | 3600 ms |
Bandwidth determines target window size:
Higher bandwidth links require larger cwnd to fully utilize. The number of doublings increases logarithmically:
Combined effect:
The time to fully utilize a link is:
Time = RTT × log₂(BDP / Initial_cwnd)
Time = RTT × log₂(Bandwidth × RTT / Initial_cwnd)
Interestingly, this means higher RTT actually speeds up slow start in terms of bytes per RTT (since more bytes can be in flight), but slows it down in wall-clock time (since each doubling takes longer).
Practical implications:
Geostationary satellite links have ~600ms RTT. Slow start from IW=10 takes over 3.5 seconds to reach reasonable capacity. For interactive applications, this delay is unacceptable. Solutions include: Performance Enhancing Proxies (PEPs) that split TCP connections, pre-warmed connection pools, or QUIC with 0-RTT resume that can skip slow start entirely.
The exponential growth formula assumes one ACK per segment sent. In reality, ACK behavior varies significantly and affects growth rate:
Delayed ACKs:
Most TCP implementations use "delayed ACKs" (RFC 1122): instead of ACKing every segment immediately, the receiver waits up to 200ms or until a second segment arrives. This means:
With delayed ACKs, growth factor becomes approximately 1.5× per RTT instead of 2×.
Appropriate Byte Counting (ABC):
RFC 3465 introduced Appropriate Byte Counting as a countermeasure. Instead of incrementing cwnd by 1 MSS per ACK, the sender increments based on bytes acknowledged:
cwnd += min(N, SMSS)
where N is the number of previously unacknowledged bytes covered by this ACK. This restores the intended doubling behavior even with delayed ACKs.
| ACK Strategy | ACKs per cwnd segments | cwnd Increase per RTT | Effective Growth Factor |
|---|---|---|---|
| Immediate ACK (per segment) | cwnd | cwnd × MSS | 2.0× per RTT |
| Delayed ACK (every 2) | cwnd / 2 | cwnd/2 × MSS | 1.5× per RTT |
| Delayed + ABC | cwnd / 2 | cwnd × MSS (via byte count) | 2.0× per RTT |
| Stretch ACKs (every N) | cwnd / N | cwnd/N × MSS | (1 + 1/N)× per RTT |
| Stretch + ABC | cwnd / N | cwnd × MSS (via byte count) | 2.0× per RTT |
ACK Division Attacks:
Before ABC was widely deployed, malicious receivers could exploit ACK-based cwnd growth:
ABC and later defenses (limiting cwnd increment per ACK) mitigated this attack.
Modern behavior:
Most current TCP stacks use some form of byte counting or hybrid approach:
The net effect is that modern implementations achieve close to the theoretical 2× growth per RTT regardless of receiver ACK behavior.
Delayed ACKs reduce protocol overhead (fewer packets for the same data) and allow piggybacking data on ACKs. The 200ms delay timer ensures responsiveness for interactive traffic. While delayed ACKs complicate slow start, they're essential for overall TCP efficiency—especially for bidirectional traffic like interactive applications.
To appreciate why exponential growth (specifically doubling) is optimal, let's compare it against alternative strategies:
Strategy 1: Linear Growth (Additive Increase)
Add one MSS per RTT regardless of ACKs:
Strategy 2: Quadratic Growth
Double the increment each RTT:
Strategy 3: Multiplicative Growth (Slow Start)
Multiply cwnd by k each RTT:
| Strategy | Formula | RTTs to Target | Waste at Overshoot | Suitability |
|---|---|---|---|---|
| Linear (add 1) | 10 + n | 999,990 RTTs | ~0% | Unusable in practice |
| Linear (add 100) | 10 + 100n | 9,999 RTTs | ~0.01% | Still far too slow |
| Polynomial (n²) | 10 + n² | 1,000 RTTs | ~0.1% | Better but still slow |
| Exponential (×1.5) | 10 × 1.5^n | ~28 RTTs | 33% | Reasonable but slower |
| Exponential (×2) | 10 × 2^n | ~17 RTTs | 50% | Standard slow start |
| Exponential (×3) | 10 × 3^n | ~11 RTTs | 67% | Too aggressive |
| Immediate max | 1,000,000 | 0 RTTs | 99.999% | Causes congestion collapse |
The Goldilocks principle:
Exponential growth with k=2 represents the "just right" balance:
Why not mix strategies?
Some protocols use hybrid approaches (e.g., slow start then linear). The transition point (ssthresh) captures where exponential growth becomes too risky. This hybrid is exactly what TCP does—slow start finds the rough capacity, then congestion avoidance fine-tunes around it.
Pure exponential (slow start with no ssthresh) would repeatedly overshoot, causing periodic heavy losses. Pure linear (no slow start) would waste link capacity for minutes on high-speed connections. The combination leverages each strategy's strengths.
Van Jacobson's insight was that a remarkably simple rule—increment cwnd by 1 per ACK—produces sophisticated emergent behavior (exponential probing). No special cases, no state machines, just one line of code in the fast path. This simplicity is why slow start has survived unchanged at its core for 35 years.
Let's examine how exponential growth manifests in practical scenarios:
Example 1: Loading a web page
A user in New York requests a page from a server in San Francisco (RTT ≈ 70ms). The HTML document is 100KB.
Time 0ms: Connection established, cwnd=10 (14.6KB)
Time 0ms: First 10 segments sent (14.6KB)
Time 70ms: ACKs arrive, cwnd=20 (29.2KB), next 20 segments sent
Time 140ms: ACKs arrive, cwnd=40 (58.4KB), next 40 segments sent
Time 210ms: All 100KB sent and acknowledged
Total time: ~210ms (3 RTTs)
Without slow start (linear growth from 1):
Example 2: Large file download
Downloading a 1GB file over a 100 Mbps connection (RTT 30ms, BDP = 375KB ≈ 257 segments).
RTT 0: cwnd=10, throughput≈3.9 Mbps (10×1460×8/0.03)
RTT 1: cwnd=20, throughput≈7.8 Mbps
RTT 2: cwnd=40, throughput≈15.6 Mbps
RTT 3: cwnd=80, throughput≈31.1 Mbps
RTT 4: cwnd=160, throughput≈62.2 Mbps
RTT 5: cwnd=320, exceeds BDP, loss likely
RTT 6: cwnd stabilizes around 257, full 100 Mbps
Total ramp-up: ~180ms (6 RTTs)
Remaining transfer at full speed: 1GB/100Mbps ≈ 80 seconds
Slow start overhead: <0.3%
Example 3: Short API call
A microservice makes a request to another service (RTT 1ms, response 2KB).
Time 0ms: cwnd=10 (14.6KB), request+response fits in one RTT
Time 1ms: Response complete
With IW=10, the entire exchange fits in the initial window. Slow start never actually "starts"—it's effectively bypassed. This is why modern IW=10 dramatically helps microservices architectures.
Example 4: Video streaming
Streaming 4K video at 25 Mbps (RTT 50ms).
RTT 0: cwnd=10, throughput≈2.3 Mbps
RTT 1: cwnd=20, throughput≈4.7 Mbps
RTT 2: cwnd=40, throughput≈9.4 Mbps
RTT 3: cwnd=80, throughput≈18.7 Mbps
RTT 4: cwnd=160, throughput≈37.4 Mbps (exceeds 25 Mbps needed)
Time to sustainable rate: ~200ms
Video players buffer ahead to absorb this ramp-up time, typically keeping 2-10 seconds buffered.
Slow start can harm performance in specific scenarios: 1) Highly interactive applications sending small messages frequently (each message may restart slow start if idle timeout triggers). 2) High-frequency trading where microseconds matter. 3) Satellite links where 3+ seconds of ramp-up is unacceptable. Solutions include connection pooling, TFO (TCP Fast Open), and specialized protocols like QUIC with 0-RTT.
We've deeply analyzed the exponential growth behavior of TCP slow start. Let's consolidate the key insights:
What's next:
Exponential growth cannot continue forever—at some point, it must stop to prevent repeated losses. The next page explores ssthresh (slow start threshold), the mechanism that captures network capacity estimates and controls when slow start yields to the more conservative congestion avoidance phase.
You now understand the mathematical foundations and practical implications of TCP slow start's exponential growth. This doubling behavior—elegant in its simplicity yet powerful in its effect—is why new connections can quickly discover path capacity across the enormous range of network speeds. Next, we'll see how ssthresh controls this growth to prevent repeated collisions with network limits.