| Internet-Draft | cuback | September 2026 |
| Oku | Expires 29 March 2027 | [Page] |
- Workgroup:
- Congestion Control Working Group
- Internet-Draft:
- draft-kazuho-ccwg-cuback-00
- Published:
- Intended Status:
- Standards Track
- Expires:
CUBACK: CUBIC Driven by the ACK Clock
Abstract
This document specifies Cuback, an ACK-driven reformulation of CUBIC congestion control. It simplifies implementation by replacing CUBIC's mutable time- and ACK-driven state with pure functions over immutable per-epoch parameters, for which test vectors are provided. Congestion-window growth uses the same ACK-driven mechanism as Reno, removing several sources of implementation error. When entering a high-capacity path, a newcomer converges on its share sooner than under CUBIC, so short flows complete earlier.¶
Discussion Venues
This note is to be removed before publishing as an RFC.¶
Discussion of this document takes place on the Congestion Control Working Group Working Group mailing list (ccwg@ietf.org), which is archived at https://mailarchive.ietf.org/arch/browse/ccwg/.¶
Source for this draft and an issue tracker can be found at https://github.com/kazuho/draft-kazuho-ccwg-cuback.¶
Status of This Memo
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 29 March 2027.¶
Copyright Notice
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
1. Introduction
CUBIC [CUBIC] is a widely deployed congestion-control algorithm that improves scalability over Reno [RENO] by defining congestion-window growth as a cubic function of elapsed time. Its implementation, however, is more complex than the growth function itself might suggest.¶
During congestion avoidance, a CUBIC sender maintains two independently evolving window estimates: the time-driven cubic window and the ACK-driven Reno-friendly window. The sender must update both estimates and select the larger of the two. In addition, because the cubic window advances with wall-clock time, the sender needs mutable epoch state and a state machine that pauses the cubic clock when the sender ceases to be congestion-control limited and resumes it when transmission becomes limited by the congestion window again. Correctly maintaining these states complicates implementations and their interaction with application-limited periods and other congestion-control transitions.¶
This document specifies Cuback, an ACK-driven reformulation of CUBIC. Cuback expresses both the cubic and Reno-friendly growth functions in terms of the amount of data that must be acknowledged for the congestion window to reach a given value. The parameters defining those functions are established at the beginning of a congestion-avoidance epoch and remain immutable for the duration of that epoch. Congestion-window growth can therefore be calculated from a pure function of the current congestion window and the per-epoch parameters, without maintaining separately evolving cubic and Reno-friendly window estimates.¶
This formulation also eliminates the need to pause and resume a wall-clock-driven CUBIC epoch. When acknowledgments stop arriving, advancement along the Cuback curves stops naturally. The remaining runtime state is essentially the same as in Reno: the sender tracks how many additional bytes need to be acknowledged before increasing the congestion window by one MSS.¶
Cuback therefore retains the characteristic growth behavior of CUBIC while reducing congestion avoidance to the same ACK-driven window-increase mechanism used by Reno. In both Reno and Cuback, acknowledged bytes are accumulated until enough have been received to increase the congestion window by one MSS. The difference lies only in how the required number of acknowledged bytes is calculated: Reno derives it directly from the current congestion window, whereas Cuback obtains it by evaluating the pure functions defined in this document.¶
What remains of CUBIC's complexity is confined to a pure function of the congestion window and the per-epoch immutables, which can be validated directly from its inputs and outputs without exercising a sequence of state-machine transitions. An implementation can therefore be checked against known values, and is correspondingly less prone to error. This document includes test vectors for that purpose.¶
The use of the ACK clock has a performance benefit as well. When a new flow joins a high-capacity path already carrying an established flow, it acquires its share more rapidly than it would under CUBIC. Cuback derives its growth rate from the congestion window and round-trip time recorded at the previous congestion event. Because the cubic function gives a flow with a smaller window a larger proportional increase, a newcomer gains share; its ACK rate, and hence its ACK clock, runs faster than originally anticipated. This gain compounds over successive round trips, and reduces the time to completion of short flows, such as the delivery of HTTP objects. The advantage diminishes as the flow's rate converges on the value it recorded.¶
Cuback alters only the congestion avoidance stage of CUBIC. All other behavior specified in [CUBIC] applies unchanged.¶
2. Conventions and Definitions
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
This document uses the notation of [CUBIC], in which window sizes are expressed in segments. An implementation that counts bytes instead scales each window value by SMSS, divides the sums appearing in Section 3 by SMSS, and expresses the lower bound on D(w) as 2 * SMSS.¶
3. Cuback Congestion Avoidance
During congestion avoidance, a Reno sender accumulates the data acknowledged and increases cwnd by one segment each time a threshold D(cwnd) is reached:¶
on entering congestion avoidance:
pending = 0
on receiving an acknowledgement, while cwnd >= ssthresh:
pending = pending + segments_acked
while pending >= D(cwnd):
pending = pending - D(cwnd)
cwnd = cwnd + 1
¶
In Reno, that threshold is the congestion window itself:¶
D_reno(cwnd) = cwnd¶
Cuback retains the mechanism above unchanged and replaces only the threshold, where A(w) is the amount of data, counted from the beginning of the current congestion avoidance stage, that has to be acknowledged for the congestion window to reach w:¶
D(w) = max(2, A(w + 1) - A(w))
A(w) = min(A_cubic(w), A_reno(w))
A_cubic(w) = bandwidth * (K(W_max, cwnd_epoch) + cbrt((w - W_max) / C))
A_reno(w) = (w - cwnd_epoch) * (w + cwnd_epoch - 1)
/ (2 * alpha_cubic) w <= W_max
= (W_max - cwnd_epoch) * (W_max + cwnd_epoch - 1)
/ (2 * alpha_cubic)
+ (w - W_max) * (w + W_max - 1) / 2 w > W_max
K(W_max, cwnd_epoch) = cbrt((W_max - cwnd_epoch) / C)
¶
W_max, bandwidth, and cwnd_epoch are fixed when the congestion avoidance stage begins and do not change until the next congestion event:¶
-
W_max is as defined in Section 4.1.2 of [CUBIC]. It is set to cwnd_prior, the congestion window before the reduction at the congestion event that began the stage.¶
-
bandwidth is an estimate of the rate at which the bottleneck delivers data, in segments per second. It is set at the congestion event to cwnd_prior divided by the smoothed round-trip time.¶
-
cwnd_epoch is the congestion window at the beginning of the congestion avoidance stage, as defined in Section 4.1.2 of [CUBIC].¶
alpha_cubic, beta_cubic, and C are the constants defined in [CUBIC], with recommended values:¶
beta_cubic = 0.7 alpha_cubic = 0.529 # 3 * (1 - beta_cubic) / (1 + beta_cubic) C = 0.4¶
A sender that derives ssthresh from flight_size rather than from cwnd (Section 4.6 of [CUBIC]) MUST use flight_size in place of cwnd_prior throughout this section. W_max, cwnd_epoch, and bandwidth MUST be derived from the same quantity.¶
When congestion avoidance is entered without a congestion event, such as on exiting slow start, the parameters are established at that point instead. cwnd is not reduced, so W_max equals cwnd_epoch and K is zero, and A(w) describes growth along the convex region alone.¶
The expressions above are derived in Appendix A.¶
3.1. Fast Convergence
Fast convergence is applied as described in Section 4.7 of [CUBIC]: on a congestion event, if cwnd is below W_max, W_max is reduced before cwnd is:¶
W_max = cwnd * (1 + beta_cubic) / 2¶
Section 4.3 of [CUBIC] keys the switch to alpha_cubic = 1 to cwnd_prior rather than to W_max, so A_reno in Section 3 uses cwnd_prior in its place; a sender that does not retain it can recover it as cwnd_epoch / beta_cubic.¶
3.2. Spurious Congestion Events
When a congestion event is determined to have been spurious (Section 4.9 of [CUBIC]), a sender that reverts cwnd and ssthresh MUST also restore W_max, bandwidth, and cwnd_epoch to the values they held before the event.¶
3.3. Application-Limited Senders
The application-limited clock handling required by Section 4.2 of [CUBIC] has no counterpart in Cuback, there being no clock to pause and resume.¶
The rules of [I-D.ietf-ccwg-ratelimited-increase], which update [CUBIC], bound the increase of a rate-limited sender without any determination of its state. Even without them, growth is governed by acknowledged data rather than by elapsed time: an idle sender accrues no growth, while a sender that continues to deliver data below cwnd grows only in proportion to that delivered data; see Section 4.3.¶
4. Relationship to RFC 9438
4.1. Sensitivity to the Bandwidth Estimate
bandwidth scales A_cubic linearly, so an error in it is an error in the rate at which the cubic curve is traversed: an underestimate traverses the curve more quickly than elapsed time would, and an overestimate more slowly. The round-trip time it is derived from has to describe the path as it was while cwnd was at its peak, with the bottleneck queue at its deepest.¶
The error is bounded in either direction by mechanisms already present. An overstated bandwidth makes A_cubic large, and A_reno carries no bandwidth term at all, so A(w) becomes A_reno(w): the flow then grows at the Reno-friendly rate, and no error in bandwidth can slow it below that. An understated bandwidth makes A_cubic small, and the lower bound of two segments on D(w) caps growth at half the congestion window per round trip, which [CUBIC] adopts to keep the increase below that of slow start.¶
Between those bounds the estimate matters only at windows large enough for the cubic curve to govern. Windows that large deliver many acknowledgements per round trip, and a QUIC sender takes a round-trip sample from every acknowledgement that advances the largest acknowledged packet number [RFC9002], so the smoothed round-trip time follows the path closely by the time the estimate matters.¶
4.2. Convergence under the ACK Clock
Deriving the clock from acknowledgements reinforces the convergence that AIMD provides, because bandwidth records the rate the flow was achieving before it reduced. A flow holding a large share gives up the most at a congestion event, so the rate it goes on to achieve falls below the value it recorded and its clock runs slow. A flow arriving with a small share records a correspondingly small rate and then achieves more than it recorded, so its clock runs fast. The congestion window advances fastest for the flow gaining share and slowest for the one giving it up.¶
The curve advances at the ratio of the rate the flow is currently achieving to the rate recorded at the last congestion event. A flow that has since doubled its share traverses the curve twice as fast as elapsed time would carry it; one that has halved its share, half as fast. The advantage therefore decays as the achieved rate converges on the recorded one.¶
4.3. Sensitivity to the Timing of Clock Transitions
Section 4.2 of [CUBIC] requires that elapsed time exclude periods during which cwnd was not updated because the sender was application limited. A CUBIC sender therefore has to pause its clock as the flow becomes application limited and resume it as the flow ceases to be, and an error at either edge is an error in the elapsed time the curve is read against.¶
Treating an application-limited sender as congestion-window limited leaves the clock running. W_cubic(t) advances while the flow is not filling cwnd, and cwnd grows on evidence the path never supplied; Section 5.8 of [CUBIC] names the consequence, that W_cubic(t) "might be very high after restarting from these periods". Treating a congestion-window-limited sender as application limited stops the clock instead, so the elapsed time that should have advanced the curve is never accrued and the flow grows too slowly.¶
Neither edge is easy to observe. A sender might emit as many datagrams as it can in one pass and so appear limited by the congestion controller, even though its bytes in flight are below cwnd. Conversely, an in-stack pacer might withhold emission after a train of acknowledgements, leaving bytes in flight below cwnd although the connection is still limited by the congestion window. [I-D.ietf-ccwg-ratelimited-increase] updates [CUBIC], mitigating the impact of such overgrowth by capping cwnd at a value derived from the largest FlightSize observed, but a CUBIC sender is still required to pause and resume its clock at the correct moments.¶
Cuback has no clock to stop or start. When the sender stops, the advance of A(w) stops with it: no acknowledgements arrive, and nothing accrues to be caught up when transmission resumes. The case in which a mistimed CUBIC clock does the most damage therefore does not arise.¶
A sender that keeps sending but holds less than cwnd in flight is not protected altogether, only bounded. Growth is governed by the amount of data acknowledged, as in Reno, so cwnd advances in proportion to what the path delivered rather than in proportion to elapsed time. Applying [I-D.ietf-ccwg-ratelimited-increase] caps what remains, holding cwnd to what one window of the largest FlightSize observed would yield.¶
4.4. The Reno-Friendly Estimate
Section 4.3 of [CUBIC] advances W_est on each acknowledgement:¶
W_est = W_est + alpha_cubic * segments_acked / cwnd¶
where cwnd is the congestion window in effect. While the cubic curve governs, that window is larger than W_est, so the growth of W_est depends on the trajectory of the other curve. This coupling cannot be expressed as a function of W_est alone, and A_reno instead inverts the standalone recurrence, in which the divisor is the Reno-friendly window itself.¶
Where A_reno is the smaller of the two curves, cwnd is the Reno-friendly window and the two formulations coincide exactly: both increase the window by one segment for every cwnd / alpha_cubic segments acknowledged. They differ only in where that region is entered. Because the standalone recurrence uses the smaller divisor, it advances faster while the cubic curve governs, and a Cuback sender enters the Reno-friendly region somewhat earlier than a sender following [CUBIC].¶
4.5. Smoothing the Cubic Growth
Section 4.4 of [CUBIC] and Section 4.5 of [CUBIC] advance cwnd toward a target one round trip ahead, W_cubic(t + RTT), rather than assigning W_cubic(t) to cwnd directly. Because the CUBIC curve is driven by elapsed time, the growth owed at an acknowledgement depends on how long has passed since the previous one, and assigning the curve directly would turn a gap in acknowledgements into a step in cwnd, which is a burst. The one-round-trip target, together with the per-acknowledgement increase of (target - cwnd) / cwnd, spreads one round trip of the curve across one round trip of acknowledgements, making the increase proportional to the data acknowledged however the acknowledgements are distributed in time.¶
Cuback needs no such smoothing. D(w) is a function of acknowledged data alone, so a gap in acknowledgements accrues no growth to be caught up, and the increase is proportional to the ack stream by construction.¶
4.6. Calculation Overhead
A_reno is arithmetic, but A_cubic requires a cube root. [CUBIC] evaluates one when the epoch begins, to obtain K, and thereafter only cubes; Cuback evaluates one whenever it computes D(w).¶
Storing the data still needed to increase the window by one segment, rather than recomputing D(w) on every acknowledgement, moves the evaluation from once per acknowledgement to once per increase. Each increase then evaluates three roots: K, A_cubic(w), and A_cubic(w + 1).¶
Calculating a cube root to the accuracy required here is cheap. The approximation must be continuous in slope, adjacent evaluations being differenced, and one meeting that condition is given in Appendix C. It runs in about nine cycles on contemporary hardware, against roughly nine hundred to encrypt a 1460-byte segment through an AES-GCM-128 pipeline at 1.6 bytes per cycle. In the worst case, the window growing by one segment for every segment acknowledged, the overhead is therefore around three percent of the cost of encrypting the data that drove it; during congestion avoidance the window typically grows far more slowly than that.¶
Furthermore, K is fixed for the duration of the epoch and could be retained rather than recomputed, and A(w + 1) for one increase is A(w) for the next and could be carried forward. Together these leave a single cube root per increase.¶
5. Security Considerations
TODO Security¶
6. IANA Considerations
This document has no IANA actions.¶
7. References
7.1. Normative References
- [CUBIC]
- Xu, L., Ha, S., Rhee, I., Goel, V., and L. Eggert, Ed., "CUBIC for Fast and Long-Distance Networks", RFC 9438, DOI 10.17487/RFC9438, , <https://www.rfc-editor.org/rfc/rfc9438>.
- [RFC2119]
- Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/rfc/rfc2119>.
- [RFC8174]
- Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/rfc/rfc8174>.
7.2. Informative References
- [I-D.ietf-ccwg-ratelimited-increase]
- Welzl, M., Henderson, T., Fairhurst, G., and M. P. Tahiliani, "Increase of the Congestion Window when the Sender Is Rate-Limited", Work in Progress, Internet-Draft, draft-ietf-ccwg-ratelimited-increase-11, , <https://datatracker.ietf.org/doc/html/draft-ietf-ccwg-ratelimited-increase-11>.
- [RENO]
- Allman, M., Paxson, V., and E. Blanton, "TCP Congestion Control", RFC 5681, DOI 10.17487/RFC5681, , <https://www.rfc-editor.org/rfc/rfc5681>.
- [RFC9002]
- Iyengar, J., Ed. and I. Swett, Ed., "QUIC Loss Detection and Congestion Control", RFC 9002, DOI 10.17487/RFC9002, , <https://www.rfc-editor.org/rfc/rfc9002>.
Appendix A. Derivation of the Increase Function
The lower bound of two segments in D(w) limits the increase to one half of the congestion window per round trip: cwnd segments are acknowledged per round trip, and each one-segment increase consumes at least two of them. It is the counterpart of the constraint target <= 1.5 * cwnd in Section 4.4 of [CUBIC] and Section 4.5 of [CUBIC]. No lower bound on the increase is needed, as A(w) is monotonically increasing and D(w) is therefore always positive.¶
CUBIC sets its congestion window to the larger of two window growth functions. Both are monotonically increasing, so their maximum is monotonically increasing as well, and the inverse of that maximum is the minimum of the two inverses. A(w) is therefore obtained by inverting each function separately and taking the smaller of the two results.¶
A_cubic is the inverse of W_cubic(t) of Section 4.2 of [CUBIC], multiplied by bandwidth to express elapsed time as acknowledged data. That the curve is zero at w = cwnd_epoch follows from the definition of K.¶
Sampling bandwidth as cwnd_prior / RTT is the rate at which the flow was delivering data over the round trip preceding the congestion event. It carries an identity:¶
bandwidth * RTT = cwnd_prior¶
While the congestion window is at cwnd_prior, one round trip of acknowledgements advances A(w) by exactly one round trip along the underlying cubic curve.¶
A_reno is the inverse of the Reno-friendly increase of Section 4.3 of [CUBIC]: one segment for every cwnd / alpha_cubic segments acknowledged, with alpha_cubic replaced by one once the window reaches W_max. Summing those thresholds over the segment-spaced windows from cwnd_epoch to w yields the expression in Section 3.¶
Differencing A_reno recovers exactly the threshold from which it was built:¶
A_reno(w + 1) - A_reno(w) = w / alpha_cubic¶
The Reno-friendly region is therefore not a separate mechanism in Cuback. It is the value that D(w) takes wherever A_reno is the smaller of the two curves. The closed form exists only so that the two curves can be compared.¶
Differencing A_cubic eliminates K, so an implementation need not evaluate it when the cubic curve is the smaller of the two:¶
A_cubic(w + 1) - A_cubic(w) = bandwidth * (cbrt((w + 1 - W_max) / C) - cbrt((w - W_max) / C))¶
Appendix B. Test Vectors
A(w) and D(w) are pure functions of the congestion window and the parameters of Section 3, so they can be checked directly. Both vectors below use the recommended constants, with alpha_cubic taken as 3 * (1 - beta_cubic) / (1 + beta_cubic) rather than the rounded 0.529.¶
The first exercises the Reno-friendly curve. bandwidth is large enough here that A_cubic exceeds A_reno at every window shown, so A(w) is A_reno(w) throughout. The window reaches cwnd_prior at 10 segments, where alpha_cubic is replaced by one and each further segment costs exactly its own window in acknowledged data.¶
cwnd_epoch = 7 segments cwnd_prior = 10 segments W_max = 10 segments RTT = 0.1 s bandwidth = 100 segments/s w A(w) D(w) 7 0.0000 13.2222 8 13.2222 15.1111 9 28.3333 17.0000 10 45.3333 10.0000 11 55.3333 11.0000 12 66.3333¶
The second exercises the cubic curve, the parameters being chosen so that K is exactly 10 seconds. A_reno exceeds A_cubic at every window shown, so A(w) is A_cubic(w) throughout. A(W_max) divided by bandwidth recovers K, and the data needed to climb the 50 segments below W_max equals that needed for the 50 above, the curve being point-symmetric about W_max.¶
cwnd_epoch = 600 segments
cwnd_prior = 1000 segments
W_max = 1000 segments
RTT = 0.1 s
bandwidth = 10000 segments/s
K = 10 s
w A(w)
600 0
950 50 000
1000 100 000
1050 150 000
1400 200 000
¶
D(w) on the cubic curve is the difference of two cube roots taken at adjacent windows, so an implementation using an approximation should compare accumulated values, as above, rather than individual increases; see Section 4.6.¶
Appendix C. An Approximation of the Cube Root
The cube root function below is provided for reference and is placed in the public domain. It assumes IEEE 754 binary64. Its maximum relative error is about 3.5e-5 and the relative error of its slope is below 8.5e-4, holding each per-segment increase to roughly 0.1 percent, and it runs in about nine cycles on a Zen 3 core.¶
#define DBL2BITS(x) (((union { double f; uint64_t u; }){x}).u)
#define BITS2DBL(x) (((union { uint64_t u; double f; }){x}).f)
double approx_cbrt(double x)
{
uint64_t u = DBL2BITS(x);
uint64_t abs_u = u & 0x7fffffffffffffff;
uint64_t exp = abs_u >> 52;
if (abs_u == 0)
return x;
if (exp == 0 || exp == 0x7ff)
return cbrt(x);
int e = (int)exp - 1023;
int q = e / 3;
int r = e - 3 * q;
if (r < 0) {
r += 3;
--q;
}
uint64_t mant = abs_u & 0xfffffffffffff;
double m = BITS2DBL(mant | ((uint64_t)1023 << 52));
double y = -0.010279630422175424;
y = 0.08469719299100167 + m * y;
y = -0.2988437245358902 + m * y;
y = 0.7177663288981584 + m * y;
y = 0.5066598330689034 + m * y;
static const double cbrt2_to_r[3] = {1.0, 1.2599210498948731648,
1.5874010519681994748};
double two_to_q = BITS2DBL((uint64_t)(q + 1023) << 52);
y *= cbrt2_to_r[r] * two_to_q;
return (u >> 63) ? -y : y;
}
¶
Acknowledgments
TODO acknowledge.¶