Skip to content

SYSTEM Cited by 1 source

CUBIC congestion control

CUBIC is a loss-based congestion control algorithm (CCA) standardised in RFC 9438. It is the default congestion controller in the Linux TCP stack and the default CCA shipped by quiche, Cloudflare's open-source QUIC / HTTP/3 library — which together govern how most TCP and QUIC connections on the public Internet probe for bandwidth, back off after loss, and recover afterward (Source: sources/2026-05-12-cloudflare-when-idle-isnt-idle-how-a-linux-kernel-optimization-became-a-quic-bug).

Algorithmic core

CUBIC is the loss-based CCA family member that uses a cubic growth curve instead of Reno's linear AIMD. Like every loss-based CCA, it operates on two premises:

  1. No loss → increase sending rate. Probe for more bandwidth.
  2. Loss → back off. Assume the network is saturated, shrink the congestion window (cwnd).

Where CUBIC differs from Reno:

  • cwnd grows along the cubic function W_cubic(delta_t) = C·(delta_t − K)^3 + W_max where delta_t = now − epoch_start (see concepts/cubic-epoch), W_max is the cwnd at the last loss event, and K is a parameter.
  • Fast ramp-up far from W_max, gentler near it. This gives CUBIC better throughput on high-BDP paths than Reno.
  • Binary increase around W_max: probing past the last-known- safe window is careful, then accelerates once it looks safe.

The epoch — a load-bearing state variable

The epoch (epoch_start) is the reference timestamp CUBIC anchors its growth curve to. It is reset whenever CUBIC restarts the growth function — most notably after a loss event reduces cwnd. Between resets, delta_t = now − epoch_start grows monotonically with wall-clock time.

The canonical CUBIC bug-class, documented by both the 2017 Linux kernel fix and the 2026-05-12 quiche fix: if epoch_start is not advanced during application-idle periods, delta_t grows while no data is being sent; the next packet sent after idleness reads delta_t as huge, computes a massive bic_target, and CUBIC tries to inflate cwnd to an unreasonable value. See concepts/cubic-epoch for the full state-variable semantics.

The 2017 Linux "CUBIC after idle" fix

Jana Iyengar's initial fix was to reset epoch_start when the application resumes sending — but Neal Cardwell pointed out this would ask CUBIC to restart the growth curve from cwnd's current value, behaving as if a loss just happened. The accepted fix (Eric Dumazet / Yuchung Cheng / Neal Cardwell, commit 30927520dbae): shift epoch_start forward by the idle duration rather than resetting it — preserving the cubic growth curve's shape, just sliding it in time.

~1 week later a follow-up commit (c2e7204d180f) titled "tcp_cubic: do not set epoch_start in the future" fixed a bug in the fix — noting that "tracking idle time in bictcp_cwnd_event() is imprecise, as epoch_start is normally set at ACK processing time, not at send time".

The 2020 quiche port — and the 2026 fix

In 2020, Cloudflare ported the 2017 fix into quiche's on_packet_sent() using bytes_in_flight == 0 as the idle predicate and now - last_sent_time as the idle delta — but did not port the 1-week-later follow-up. At minimum cwnd (two packets), bytes_in_flight drops to zero on every ACK cycle, and last_sent_time is the start of the previous RTT, so the "idle" delta is ~RTT (not ~0), and the recovery boundary is advanced into the future — triggering the CUBIC minimum-cwnd death spiral.

Cloudflare's 2026-05-12 fix (three lines of logic) adds a last_ack_time state variable and uses max(last_ack_time, last_sent_time) as the idle-delta anchor, correctly distinguishing transient congestion-limited bytes_in_flight == 0 dips from genuine application idleness. See patterns/measure-idle-from-last-ack-not-last-send.

Seen in

Last updated · 542 distilled / 1,571 read