Deep dive
How it works
In: ACK events (segments acked, time, smoothed RTT) and congestion signals (loss, ECN mark, timeout) · Out: congestion window in segments · State: cwnd, ssthresh, w_max, k, epoch_start, w_est · Cost: O(1) per ACK; one cube root per loss.
State per connection: cwnd, ssthresh, w_max, k, epoch_start, and a Reno-equivalent estimate w_est.
- Slow start (cwnd < ssthresh): cwnd += acked segments (doubles per RTT). Linux exits early with HyStart when ACK trains or RTT growth signal the pipe is full; RFC 9406 standardizes HyStart++.
- On congestion event (3 dup ACKs / SACK loss / ECN): with fast convergence, if cwnd < w_max (bandwidth shrinking) set w_max = cwnd·(1+β)/2, else w_max = cwnd. Then ssthresh = cwnd = max(cwnd·β, 2), epoch_start = now, K = ∛((w_max − cwnd)/C).
- Congestion avoidance, on each ACK at time t = now − epoch_start:
- target = W_cubic(t + RTT), clamped to [cwnd, 1.5·cwnd].
- cwnd += (target − cwnd)/cwnd per acked segment.
- Reno-friendly region: w_est += α·acked/cwnd with α = 3(1−β)/(1+β) ≈ 0.53; if W_cubic < w_est use w_est. This keeps CUBIC at least as aggressive as Reno on short-RTT, low-BDP paths.
- RTO: cwnd = 1, ssthresh = β·cwnd, restart slow start.
cwnd
^ concave plateau convex (probing)
| W_max ---------------_________-------------- . . .
| _ -‾ ‾- _
| _ -‾ ‾--__
| β·W_max
+---|---------------------|--------------------> t
loss t = K
Code
/// CUBIC window logic in segments (RFC 9438). Time in seconds.
pub struct Cubic {
pub cwnd: f64,
pub ssthresh: f64,
w_max: f64,
k: f64,
epoch_start: Option<f64>,
w_est: f64,
}
const C: f64 = 0.4;
const BETA: f64 = 0.7;
const ALPHA: f64 = 3.0 * (1.0 - BETA) / (1.0 + BETA);
impl Cubic {
pub fn new() -> Self {
// IW10 (RFC 6928); ssthresh starts "infinite".
Cubic { cwnd: 10.0, ssthresh: f64::INFINITY, w_max: 0.0, k: 0.0, epoch_start: None, w_est: 0.0 }
}
fn w_cubic(&self, t: f64) -> f64 {
C * (t - self.k).powi(3) + self.w_max
}
pub fn on_ack(&mut self, acked: f64, now: f64, srtt: f64) {
if self.cwnd < self.ssthresh {
self.cwnd += acked; // slow start
return;
}
if self.epoch_start.is_none() {
// First CA ACK without a prior loss (e.g. after slow start exit).
self.w_max = self.w_max.max(self.cwnd);
self.k = ((self.w_max - self.cwnd) / C).cbrt();
self.w_est = self.cwnd;
self.epoch_start = Some(now);
}
let t = now - self.epoch_start.unwrap();
self.w_est += ALPHA * acked / self.cwnd;
let target = self.w_cubic(t + srtt).clamp(self.cwnd, 1.5 * self.cwnd);
if self.w_cubic(t) < self.w_est {
self.cwnd = self.w_est; // Reno-friendly region
} else {
self.cwnd += (target - self.cwnd) / self.cwnd * acked;
}
}
pub fn on_congestion(&mut self, now: f64) {
// Fast convergence: release bandwidth faster when the network shrinks.
self.w_max = if self.cwnd < self.w_max { self.cwnd * (1.0 + BETA) / 2.0 } else { self.cwnd };
self.cwnd = (self.cwnd * BETA).max(2.0);
self.ssthresh = self.cwnd;
self.w_est = self.cwnd;
self.k = ((self.w_max - self.cwnd) / C).cbrt();
self.epoch_start = Some(now);
}
pub fn on_rto(&mut self) {
self.ssthresh = (self.cwnd * BETA).max(2.0);
self.cwnd = 1.0;
self.epoch_start = None;
}
}
Complexity and constants
- O(1) per ACK; one cube root per loss. Linux avoids floating point in the kernel: a 64-entry lookup table plus one Newton–Raphson step, time scaled by 2^10.
- Average window (RFC 8312 response function): W ≈ (C(3+β)/(4(1−β)))^¼ · RTT^¾ / p^¾ ≈ 1.05·RTT^0.75 / p^0.75 (RTT in s) vs Reno's 1.22 / p^0.5, so CUBIC tolerates far higher loss at high BDP.
- Why it exists: Reno at 10 Gbit/s, 100 ms RTT, 1500 B packets needs a window of about 83k segments and a loss rate near 2·10⁻¹⁰ (one loss per ~5 billion packets, RFC 3649).
- Recovery time to W_max is K seconds: about 4.2 s for W_max = 100, 9.1 s for 1000, 19.6 s for 10 000 segments.
Efficiency
- Cost: a few multiplications per ACK; Linux uses a 64-entry table plus one Newton step instead of floating point. The bound is the network, not the CPU: throughput is cwnd / RTT and loss-limited, W ≈ 1.05 · RTT^0.75 / p^0.75 (RFC 8312) against Reno's 1.22 / p^0.5. Recovery to W_max takes K seconds: 9.1 s at 1000 segments.
- Levers:
- HyStart++ — leave slow start before a burst of losses (RFC 9406) — short and medium flows
- pacing (
fqqdisc,SO_MAX_PACING_RATE) — fewer burst losses on shallow buffers — high-rate senders - AQM + ECN (fq_codel, CAKE) — removes the standing queue CUBIC would build — shared bottlenecks
tcp_slow_start_after_idle=0— keeps cwnd across idle gaps — pooled RPC / HTTP/2 connections- RACK + DSACK undo — avoids the 30 % cut on pure reordering — reordering paths
- Naive → standard → state of the art:
- Reno, +1 segment per RTT, halve on loss · refill takes over an hour on 10 Gbit/s, 100 ms; needs loss near 2·10⁻¹⁰ (RFC 3649)
- CUBIC, cubic growth in time since loss · recovery K = 9.1 s at 1000 segments, RTT-independent
- CUBIC + HyStart++ + pacing + AQM/ECN, or BBR on lossy paths · little overshoot, short queues
Implementation notes
- Linux:
net.ipv4.tcp_congestion_control = cubic(default since 2.6.19); per socketsetsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "cubic"). - Pacing is not intrinsic to CUBIC; with the
fqqdisc orSO_MAX_PACING_RATEyou smooth bursts and reduce loss on shallow switch buffers. - Treat ECN-CE marks like loss (once per RTT). Combine with an AQM (fq_codel, CAKE) to cut the queueing delay CUBIC would otherwise create.
tcp_slow_start_after_idle=1(default) collapses cwnd after an idle RTO; disable it for long-lived request/response connections (HTTP/2, RPC pools).- Count cwnd in bytes in implementations that see variable packet sizes (QUIC stacks do).
Pitfalls
- App-limited periods: if the epoch keeps running while the sender is idle, t grows and cwnd jumps on resume. Linux fixed this in 2015 by shifting epoch_start by the idle time; copy that in your own implementation.
- Spurious loss detection (reordering) costs a 30 % cut; use RACK and undo on DSACK.
- Random loss of order 1 % caps throughput hard on high-RTT paths; BBR keeps going.
- Bufferbloat: on a 1 Gbit/s link with 100 MB of buffer, CUBIC can add hundreds of ms of queueing delay.
- Comparing CUBIC and Reno with short flows shows nothing; most web flows end in slow start, where the initial window and HyStart matter more.
Variants and alternatives
In production
Default in Linux (2.6.19+), Windows 10 1709+ and Server 2019+, macOS/iOS, Android. QUIC stacks (quiche, msquic, quinn, ngtcp2) ship CUBIC as the default or an option. Default on Linux, Windows and Apple stacks per RFC 9438.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| Linux tcp_cubic | C | torvalds/linux | n/a (kernel built in; sysctl net.ipv4.tcp_congestion_control=cubic) | — | — | — | — | net/ipv4/tcp_cubic.c; fixed-point, HyStart |
| quinn-proto | Rust | quinn-rs/quinn | cargo add quinn-proto | — | — | — | — | congestion::Cubic controller for QUIC (also NewReno, BBR) |
| quiche | Rust | cloudflare/quiche | cargo add quiche | — | — | — | — | CUBIC among its QUIC congestion controllers |
| msquic | C | microsoft/msquic | build from source (CMake) | — | — | — | — | CUBIC is the default controller |
| ngtcp2 | C | ngtcp2/ngtcp2 | build from source (autotools or CMake) | — | — | — | — | CUBIC, Reno and BBR in the library |
Sources
- Ha, Rhee, Xu, CUBIC: A New TCP-Friendly High-Speed TCP Variant, ACM SIGOPS OSR, 2008. paper
- Rhee et al., RFC 8312: CUBIC for Fast Long-Distance Networks, 2018; Xu et al., RFC 9438, 2023. 8312 · 9438
- Ha, Rhee, Taming the Elephants: New TCP Slow Start (HyStart), Computer Networks, 2011. paper
- Floyd, RFC 3649: HighSpeed TCP for Large Congestion Windows, 2003. rfc
- Balasubramanian, Chu, Dukkipati, Goyal, RFC 9406: HyStart++: Modified Slow Start for TCP, 2023. rfc