Home › Networking › Transport & congestion control
algorithm low

TCP CUBIC

Problem Senders on shared networks must choose how fast to send without knowing the free bandwidth: fill long, fast links quickly after a setback, yet back off enough that competing flows get a fair share.

Loss-based TCP congestion control whose window grows as a cubic function of time since the last loss; the default on Linux, Windows and macOS.

O(1) per ACK; one cube root per loss event
tcpcongestion-controlloss-basedlinuxrfc-9438
Overview

Reno grows the window by one segment per RTT, so a 10 Gbit/s, 100 ms path needs over an hour to refill after one loss. CUBIC makes the window a function of wall-clock time since the last loss: a cubic curve centered on the window where the loss happened (W_max). It climbs fast when far below W_max, flattens near it (probing gently where loss happened last time), then accelerates again to find new bandwidth.

Key facts

Use when

  • General-purpose TCP; it is the default and well understood by every middlebox and operator.
  • Deep-buffered or datacenter paths where loss really means congestion.

Avoid when

  • Lossy wireless or long-haul paths with random loss; use BBR.
  • Latency-sensitive traffic sharing a bloated buffer; CUBIC keeps the queue full.

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.

  1. 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++.
  2. 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).
  3. 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.
  4. 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 (fq qdisc, 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 socket setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "cubic").
  • Pacing is not intrinsic to CUBIC; with the fq qdisc or SO_MAX_PACING_RATE you 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

Reno / NewRenosimplest, baseline in RFC 9002 for QUIC; fine for low BDP.
BBRmodel-based; higher throughput under random loss, lower queues; fairness to CUBIC varies with buffer size.
DCTCPdatacenter, ECN-proportional backoff; needs switch ECN marking at low thresholds.
Compound TCPolder Windows default (loss + delay hybrid).

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

namelangrepoinstallverdictlicencehealthas_ofnotes
Linux tcp_cubicCtorvalds/linuxn/a (kernel built in; sysctl net.ipv4.tcp_congestion_control=cubic)————net/ipv4/tcp_cubic.c; fixed-point, HyStart
quinn-protoRustquinn-rs/quinncargo add quinn-proto————congestion::Cubic controller for QUIC (also NewReno, BBR)
quicheRustcloudflare/quichecargo add quiche————CUBIC among its QUIC congestion controllers
msquicCmicrosoft/msquicbuild from source (CMake)————CUBIC is the default controller
ngtcp2Cngtcp2/ngtcp2build 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

Used by

userspace tcp ip stack · plannedTCP/IP in user space (smoltcp/lwIP style)

Alternatives and related