RFC 9937

Proportional Rate Reduction (PRR)

๐Ÿ“… published December 2025, from ietf

Proportional Rate Reduction (PRR) ๐ŸŽ€

Part 1 of 6 ๐Ÿ’–

okay bestie, RFC 9937 is here and it's a whole glow-up ๐Ÿฆ‹ here's the title block tea โ˜•

  • RFC 9937, from the IETF, titled Proportional Rate Reduction (PRR)
  • Authors: M. Mathis, N. Cardwell, Y. Cheng, and N. Dukkipati, all at Google, Inc. ๐Ÿ‘‘
  • Category: Standards Track
  • ISSN: 2070-1721
  • Date: December 2025
  • Obsoletes: 6937 (this replaces the Experimental PRR from RFC 6937 ๐Ÿ’…)
  • Updates: nothing is listed

Abstract โœจ

okay so this doc specs a Standards Track version of PRR, and it obsoletes the Experimental one from RFC 6937 ๐ŸŒธ PRR controls how much data TCP (or other transport protocols) sends during fast recovery. The goal is for the actual flight size to be as close as possible to ssthresh by the end of recovery, with ssthresh set by the congestion control algorithm. so precise, periodt ๐Ÿ’…

Status of This Memo ๐Ÿ“œ

This is an Internet Standards Track document.

This document is a product of the Internet Engineering Task Force (IETF). It represents the consensus of the IETF community. It has received public review and has been approved for publication by the Internet Engineering Steering Group (IESG). Further information on Internet Standards is available in Section 2 of RFC 7841.

Information about the current status of this document, any errata, and how to provide feedback on it may be obtained at https://www.rfc-editor.org/info/rfc9937.

Copyright (c) 2025 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. ๐Ÿซถ

Table of Contents ๐Ÿ—บ๏ธ

1.  Introduction
2.  Conventions
3.  Definitions
4.  Changes Relative to RFC 6937
5.  Relationships to Other Standards
6.  Algorithm
  6.1.  Initialization Steps
  6.2.  Per-ACK Steps
  6.3.  Per-Transmit Steps
  6.4.  Completion Steps
7.  Properties
8.  Examples
9.  Adapting PRR to Other Transport Protocols
10. Measurement Studies
11. Operational Considerations
  11.1.  Incremental Deployment
  11.2.  Fairness
  11.3.  Protecting the Network Against Excessive Queuing and
         Packet Loss
12. IANA Considerations
13. Security Considerations
14. References
  14.1.  Normative References
  14.2.  Informative References
Appendix A.  Strong Packet Conservation Bound
Acknowledgments
Authors' Addresses

1. Introduction โœจ

okay first, the legend Van Jacobson came up with: the packet conservation principle [Jacobson88] ๐Ÿ‘‘ It's a self clock process. N data segments get delivered to the receiver, which generates ACKs, and the data sender uses those ACKs as the clock to trigger sending N more data segments into the network ๐Ÿ•ฐ๏ธ

Reno [RFC5681] and CUBIC [RFC9438] are built on this self clock idea. They control sending with a congestion window ("cwnd") that limits "inflight", which is the volume of data a connection estimates is in flight in the network at a given time. These algorithms also require connections to reduce their cwnd in response to packet losses ๐Ÿ“‰ Fast recovery (see [RFC5681] and [RFC6675]) is the algorithm that makes this cwnd reduction using feedback from ACKs. Its stated goal is to maintain the sender's self clock by relying on the ACKs returning during recovery to clock more data into the network. Without Proportional Rate Reduction (PRR), fast recovery usually adjusts the window by waiting for a large fraction of a round-trip time (RTT) to pass before sending any data. That's giving stall ๐Ÿ™ˆ (for Reno [RFC5681], that's one half round-trip time of ACKs, and for CUBIC [RFC9438], it's 30% of a round-trip time.)

[RFC6675] makes fast recovery with Selective Acknowledgment (SACK) [RFC2018] more accurate by computing "pipe", a sender-side estimate of the number of bytes still outstanding in the network. With [RFC6675], fast recovery sends data as needed on each ACK to let pipe rise to match ssthresh, the target window size for fast recovery as determined by the congestion control algorithm. This protects fast recovery from timeouts in many heavy-loss cases ๐Ÿ›ก๏ธ However, [RFC6675] has two big drawbacks. First, because it makes a large multiplicative decrease in cwnd at the start of fast recovery, it can cause a timeout if the entire second half of the window of data or ACKs is lost. Second, a single ACK carrying a SACK option that implies a large quantity of missing data can cause a step discontinuity in the pipe estimator, which can make Fast Retransmit send a large burst of data ๐Ÿ’€

PRR regulates the transmission process during fast recovery in a way that avoids these excess window adjustments. Transmissions progress smoothly, and at the end of recovery, the actual window size will be as close as possible to ssthresh ๐Ÿฆ‹

PRR's approach is inspired by Van Jacobson's packet conservation principle. As much as possible, PRR relies on the self clock process and is only slightly affected by the accuracy of estimators, like the estimate of the volume of in-flight data. This is what gives the algorithm its precision when other estimators get uncertain ๐ŸŽฏ

When inflight is above ssthresh, PRR reduces inflight smoothly toward ssthresh by clocking out transmissions at a rate in proportion to both the delivered data and ssthresh ๐ŸŒŠ

When inflight is less than ssthresh, PRR adaptively chooses between one of two Reduction Bounds to limit the total window reduction due to all mechanisms, including transient application stalls and the losses themselves. As a baseline, to be cautious when there may be considerable congestion, PRR uses its Conservative Reduction Bound (CRB), which is strictly packet conserving ๐Ÿง˜ When recovery seems to be progressing well, PRR uses its Slow Start Reduction Bound (SSRB), which is more aggressive than PRR-CRB by at most one segment per ACK. PRR-CRB meets the Strong Packet Conservation Bound described in Appendix A. However, when used in real networks as the sole approach, it does not perform as well as the algorithm described in [RFC6675], which proves to be more aggressive in a significant number of cases. PRR-SSRB offers a compromise by allowing a connection to send one additional segment per ACK, relative to PRR-CRB, in some situations. Although PRR-SSRB is less aggressive than [RFC6675] (transmitting fewer segments or taking more time to transmit them), it outperforms due to the lower probability of additional losses during recovery ๐Ÿ“ˆ

The original definition of the packet conservation principle [Jacobson88] treated packets that are presumed to be lost (e.g., marked as candidates for retransmission) as having left the network. This idea is reflected in the inflight estimator used by PRR, but it is distinct from the Strong Packet Conservation Bound described in Appendix A, which is defined solely on the basis of data arriving at the receiver ๐Ÿง 

This document specifies several main changes from the earlier version of PRR in [RFC6937] ๐Ÿ“ First, it introduces a new adaptive heuristic that replaces a manual configuration parameter. That parameter used to determine how conservative PRR was when inflight was less than ssthresh (whether to use PRR-CRB or PRR-SSRB). Second, the algorithm specifies behavior for non-SACK connections (connections that have not negotiated SACK [RFC2018] support via the "SACK-permitted" option). Third, the algorithm ensures a smooth sending process even when the sender has experienced high reordering and starts loss recovery after a large amount of sequence space has been SACKed. Finally, this document also includes additional discussion about the integration of PRR with congestion control and loss detection algorithms ๐ŸŽ€

PRR has extensive deployment experience in multiple TCP implementations since the first widely deployed TCP PRR implementation in 2011 [First_TCP_PRR] ๐Ÿฅณ

2. Conventions ๐ŸŽ€

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. ๐ŸŒˆ

3. Definitions ๐Ÿ“š

okay so before we get into the juicy stuff, these are the terms, parameters, and state variables we're borrowing from earlier docs ๐Ÿ’… they're already defined there, so we just use them as-is ๐Ÿซถ

SND.UNA: SND.UNA is the oldest unacknowledged sequence number ๐Ÿฆ‹ It's defined in Section 3.4 of [RFC9293].

SND.NXT: SND.NXT is the next sequence number to be sent โœจ It's defined in Section 3.4 of [RFC9293].

duplicate ACK: okay this one is giving detective energy ๐Ÿ•ต๏ธโ€โ™€๏ธ An acknowledgment counts as a "duplicate ACK" or "duplicate acknowledgment" when all of these are true:

  • (a) the receiver of the ACK has outstanding data,
  • (b) the incoming acknowledgment carries no data,
  • (c) the SYN and FIN bits are both off,
  • (d) the acknowledgment number is equal to SND.UNA, and
  • (e) the advertised window in the incoming acknowledgment equals the advertised window in the last incoming acknowledgment.

This is defined in Section 2 of [RFC5681] ๐ŸŒธ

FlightSize: this is the amount of data that's been sent but not yet cumulatively acknowledged ๐Ÿš€ It's defined in Section 2 of [RFC5681].

Receiver Maximum Segment Size (RMSS): the RMSS is the size of the largest segment the receiver is willing to accept ๐ŸŽ€ This is the value in the MSS option the receiver sends during connection startup (see Section 3.7.1 of [RFC9293]). If the MSS option isn't used, it's the default of 536 bytes for IPv4 or 1220 bytes for IPv6 (see Section 3.7.1 of [RFC9293]). The size does not include the TCP/IP headers and options. The RMSS is defined in Section 2 of [RFC5681] and Section 3.8.6.3 of [RFC9293].

Sender Maximum Segment Size (SMSS): the SMSS is the size of the largest segment the sender can transmit ๐Ÿ“ This value can be based on the Maximum Transmission Unit (MTU) of the network, the path MTU discovery [RFC1191] [RFC8201] [RFC4821] algorithm, RMSS, or other factors. The size does not include the TCP/IP headers and options. This is defined in Section 2 of [RFC5681].

Receiver Window (rwnd): this is the most recently received advertised receiver window, in bytes ๐ŸชŸ At any given time, a connection MUST NOT send data with a sequence number higher than the sum of SND.UNA and rwnd. This is defined in Section 2 of [RFC5681]. no cap, that MUST NOT is doing the most ๐Ÿ˜ญ

Congestion Window (cwnd): this is a state variable that limits the amount of data a connection can send ๐Ÿงฑ At any given time, a connection MUST NOT send data if inflight (see below) matches or exceeds cwnd. This is defined in Section 2 of [RFC5681] ๐Ÿ’œ

Slow Start Threshold (ssthresh): the slow start threshold (ssthresh) state variable decides whether the slow start or congestion avoidance algorithm is used to control data transmission ๐ŸŒˆ During fast recovery, ssthresh is the target window size for a fast recovery episode, as determined by the congestion control algorithm. This is defined in Section 3.1 of [RFC5681].

PRR defines some extra variables and terms too, and these are the ones it's obsessed with ๐Ÿงš

Delivered Data (DeliveredData): this is the data sender's best estimate of the total number of bytes that the current ACK says have been delivered to the receiver since the previously received ACK ๐Ÿ“ฌ

In-Flight Data (inflight): this is the data sender's best estimate of the number of unacknowledged bytes in flight in the network, i.e., bytes that were sent and neither lost nor received by the data receiver ๐Ÿ›ซ

Recovery Flight Size (RecoverFS): this is the number of bytes the sender estimates might possibly be delivered over the course of the current PRR episode ๐Ÿ”ฎ

SafeACK: this is a local boolean variable that says the current ACK shows the recovery is making good progress ๐Ÿ“ˆ When that's true, the sender can send more aggressively, increasing inflight, if appropriate. main character energy ๐Ÿ‘‘

SndCnt: this is a local variable that tells you exactly how many bytes should be sent in response to each ACK ๐Ÿ”ข

Voluntary window reductions: this is when you choose not to send data in response to some ACKs, on purpose, to reduce the sending window size and data rate ๐Ÿซง

4. Changes Relative to RFC 6937 โœจ

okay so the biggest glow-up since [RFC6937] is a brand new heuristic ๐Ÿ’… It uses good recovery progress (for TCP, when the latest ACK advances SND.UNA and doesn't indicate that a prior fast retransmit has been lost) to pick the Reduction Bound (PRR-CRB or PRR-SSRB) ๐ŸŽ€ [RFC6937] left the choice of Reduction Bound up to the implementer, but it recommended PRR-SSRB by default. For all the environments explored in earlier PRR research, the new heuristic lines up with the old recommendation ๐Ÿฅฐ

now here's the plot twist ๐Ÿ˜ฑ The paper "An Internet-Wide Analysis of Traffic Policing" [Flach2016policing] found a crucial situation nobody had explored before, where both Reduction Bounds perform very poorly, but for different reasons ๐Ÿ’€ Under many configurations, token bucket traffic policers can suddenly start discarding a large fraction of the traffic once their tokens run out, with zero warning to the end systems ๐Ÿ™ˆ The transport congestion control never gets a chance to measure the token rate, so it sets ssthresh based on the path performance it saw earlier. That ssthresh value can cause a data rate way bigger than the token replenishment rate, which means high loss ๐Ÿ“‰ In these conditions, both Reduction Bounds are bad. PRR-CRB is too timid, sometimes causing very long recovery times at smaller windows than necessary, and PRR-SSRB is too aggressive, often causing lots of retransmissions to get lost for multiple rounds ๐Ÿ˜ฉ Both of these lead to prolonged recovery, which decimates application latency and/or goodput.

so investigating all this led to the "SafeACK" heuristic ๐Ÿฆ‹ It dynamically switches between Reduction Bounds: by default it conservatively uses PRR-CRB, and it only switches to PRR-SSRB when ACKs show the recovery is making good progress (SND.UNA is advancing without detecting any new losses) ๐ŸŒท The SafeACK heuristic got tested in Google's Content Delivery Network (CDN) [Flach2016policing] and has been implemented in Linux TCP since 2015 ๐Ÿง

This SafeACK heuristic is only invoked where losses, application-limited behavior, or other events cause the current estimate of in-flight data to fall below ssthresh. The high loss rates that make the heuristic essential are only common in the presence of heavy losses, such as traffic policers [Flach2016policing]. In these environments, the heuristic performs better than either bound by itself ๐Ÿซถ

another PRR algorithm change makes the sending process better when the sender enters recovery after a large portion of sequence space has been SACKed ๐Ÿง  That can happen when the sender has previously detected reordering, for example by using [RFC8985]. In the previous version of PRR, RecoverFS didn't properly account for sequence ranges SACKed before entering fast recovery, which made PRR send too slowly at first ๐ŸŒ With the change, PRR properly accounts for those sequence ranges. slay โœจ

yet another change is to force a fast retransmit upon the first ACK that triggers the recovery ๐Ÿšจ Previously, PRR might not allow a fast retransmit (i.e., SndCnt is 0) on the first ACK in fast recovery, depending on the loss situation. Forcing a fast retransmit matters for keeping the ACK clock going and avoiding potential retransmission timeout (RTO) events ๐Ÿ•ฐ๏ธ The forced fast retransmit only happens once during the entire recovery, and it still follows the packet conservation principles in PRR. This heuristic has been implemented since the first widely deployed TCP PRR implementation in 2011 [First_TCP_PRR] ๐ŸŽ‚

in another change, when a data sender exits recovery, it sets cwnd to ssthresh ๐Ÿ This is important for robust performance. Without setting cwnd to ssthresh at the end of recovery, and with application-limited sender behavior and some loss patterns, cwnd could end fast recovery well below ssthresh, which leads to bad performance ๐Ÿ˜ฌ In some cases that could be worse than [RFC6675] recovery, which simply sets cwnd to ssthresh at the start of recovery. This end-of-recovery behavior has been implemented since the first widely deployed TCP PRR implementation in 2011 [First_TCP_PRR], and it's similar to [RFC6675], which specifies setting cwnd to ssthresh at the start of recovery ๐Ÿ’–

since [RFC6937] was written, PRR has also been adapted to do multiplicative window reduction for non-loss-based congestion control algorithms, such as for Explicit Congestion Notification (ECN) as specified in [RFC3168] ๐ŸŒˆ This can be done by using some parts of the loss recovery state machine (in particular, the RecoveryPoint from [RFC6675]) to invoke the PRR ACK processing for exactly one round trip worth of ACKs. However, there can be interactions between PRR and approaches to Active Queue Management (AQM) and ECN, so guidance on developing and assessing congestion control mechanisms is provided in [RFC9743] ๐Ÿง

5. Relationships to Other Standards ๐Ÿค

PRR MAY be used with any congestion control algorithm that intends to make a multiplicative decrease in its sending rate over approximately the time scale of one round-trip time ๐Ÿ• This works as long as the current volume of in-flight data is limited by a congestion window (cwnd), and the target volume of in-flight data during that reduction is a fixed value given by ssthresh. In particular, PRR is applicable to both Reno [RFC5681] and CUBIC [RFC9438] congestion control ๐Ÿฆ‹ PRR is described as a modification to "A Conservative Loss Recovery Algorithm Based on Selective Acknowledgment (SACK) for TCP" [RFC6675]. It is most accurate with SACK [RFC2018] but does not require SACK ๐ŸŒธ

PRR can be used with a wide array of loss detection algorithms, and that's because PRR doesn't depend on the details of how a loss detection algorithm figures out which packets have been delivered and which have been lost ๐Ÿงฉ Upon the reception of each ACK, PRR just needs the loss detection algorithm to communicate how many packets have been marked as lost and how many have been marked as delivered. So PRR MAY be used with the loss detection algorithms specified or described in these documents: Reno [RFC5681], NewReno [RFC6582], SACK [RFC6675], Forward Acknowledgment (FACK) [FACK], and Recent Acknowledgment Tail Loss Probe (RACK-TLP) [RFC8985] ๐ŸŽ€ Because of the performance properties of RACK-TLP, including resilience to tail loss, reordering, and lost retransmissions, it is RECOMMENDED that PRR is implemented together with RACK-TLP loss recovery [RFC8985]. bestie, that's the one to pick ๐Ÿซถ

The SafeACK heuristic came about because of robust Lost Retransmission Detection that was under development in an early precursor to [RFC8985] ๐Ÿ”ฌ Without Lost Retransmission Detection, policers that cause very high loss rates are at very high risk of causing retransmission timeouts, because Reno [RFC5681], CUBIC [RFC9438], and [RFC6675] can send retransmissions significantly above the policed rate ๐Ÿ˜ญ It's a whole saga, iykyk ๐Ÿ’€

6. Algorithm ๐Ÿง

6.1. Initialization Steps ๐ŸŽ€

Okay so, the very beginning of a congestion control response episode, the one kicked off by the congestion control algorithm, is when a data sender using PRR MUST initialize the PRR state. Bestie, this is the setup moment, so don't skip it ๐Ÿ’…

When that episode starts is totally up to the congestion control algorithm ๐Ÿฆ‹ It could line up with the start of a fast recovery episode, or with a once-per-round-trip reduction when lost retransmits or lost original transmissions get detected while fast recovery is already happening. Its call, not PRR's.

The initialization is what lets a congestion control algorithm, CongCtrlAlg(), pick an ssthresh that isn't FlightSize/2. CUBIC [RFC9438] is one example of an algorithm that does this โœจ

The biggest step in initialization is working out the Recovery Flight Size (RecoverFS) ๐Ÿงš That's the number of bytes the data sender thinks could possibly get delivered during the whole PRR episode. You can picture it as a sum of four things measured at the start of the episode: inflight, the bytes cumulatively acknowledged by the ACK that triggered recovery, the bytes SACKed by that same ACK, and the bytes between SND.UNA and SND.NXT that are marked lost. RecoverFS counts losses because losses get marked using heuristics, so some packets flagged as lost might still get delivered during recovery without being retransmitted. Mood, honestly ๐Ÿ˜ญ PRR uses RecoverFS to keep the sending rate smooth. Once fast recovery starts, PRR sets RecoverFS, and it stays constant for that whole fast recovery episode ๐Ÿ”’

Here's the full initialization sequence:

   ssthresh = CongCtrlAlg()      // Target flight size in recovery
   prr_delivered = 0             // Total bytes delivered in recovery
   prr_out = 0                   // Total bytes sent in recovery
   RecoverFS = SND.NXT - SND.UNA
   // Bytes SACKed before entering recovery will not be
   // marked as delivered during recovery:
   RecoverFS -= (bytes SACKed in scoreboard)
   // Include the (common) case of selectively ACKed bytes:
   RecoverFS += (bytes newly SACKed)
   // Include the (rare) case of cumulatively ACKed bytes:
   RecoverFS += (bytes newly cumulatively acknowledged)

6.2. Per-ACK Steps ๐Ÿ’–

Every ACK that arrives while fast recovery is starting or already running gets these steps, except for the ACK that wraps up a PRR episode ๐ŸŽฌ

First up, the sender works out DeliveredData, which is its best guess at how many bytes the current ACK says reached the receiver since the last ACK. With SACK, this can be worked out exactly: take the change in SND.UNA and add the signed change in how much data is marked SACKed in the scoreboard. So if there are no SACKed ranges in the scoreboard before or after the ACK, DeliveredData is just the change in SND.UNA. Without SACK, in recovery, DeliveredData is guessed as 1 SMSS for every duplicate ACK received (meaning SND.UNA didn't move). When SND.UNA does move (a full or partial ACK), DeliveredData is the change in SND.UNA minus 1 SMSS for each duplicate ACK that came before it.

Now, no cap, there's a sneaky problem ๐Ÿ™ˆ Without SACK, a badly behaved receiver that sends extra duplicate ACKs (see [Savage99]) could try to blow up DeliveredData on purpose. To stop that, when SACK isn't in use, PRR won't let DeliveredData go up if the total bytes delivered in a PRR episode would pass the estimated data outstanding when recovery started (RecoverFS). Protect your data, babe ๐Ÿ›ก๏ธ

Next, the sender estimates inflight, which is its best guess at how many bytes are currently in flight in the network ๐ŸŒŠ For connections with SACK enabled that use [RFC6675] loss detection, they MAY use the "pipe" algorithm specified in [RFC6675]. SACK-enabled connections using RACK-TLP loss detection [RFC8985], or some other loss detection algorithm, MUST calculate inflight by starting with SND.NXT - SND.UNA, subtracting the bytes SACKed in the scoreboard, subtracting the bytes marked lost in the scoreboard, and then adding back the bytes in the scoreboard that were retransmitted after they were last marked lost. For connections without SACK, senders instead MUST subtract min(RecoverFS, 1 SMSS for each preceding duplicate ACK in the fast recovery episode) rather than subtracting the bytes SACKed in the SACK scoreboard. That min() with RecoverFS is there to protect against misbehaving receivers [Savage99] ๐Ÿง

Then the sender figures out SafeACK, a local boolean that says the current ACK showed good progress ๐ŸŒธ SafeACK is only true when the ACK cumulatively acknowledged new data and the ACK doesn't signal any more losses. For instance, an ACK that triggers a "rescue" retransmission (Section 4 of [RFC6675], NextSeg() condition 4) might be signalling more losses. When both conditions hold, recovery is clearly going well, so the sender can push harder and raise inflight if that makes sense โญ

Last step: the sender uses DeliveredData, inflight, SafeACK, and the rest of the PRR state to compute SndCnt, a local variable that says exactly how many bytes to send in response to each ACK. Then SndCnt gets used to update cwnd ๐Ÿฆ‹

The full per-ACK sequence goes like this:

   if (DeliveredData is 0)
      Return


   prr_delivered += DeliveredData
   inflight = (estimated volume of in-flight data)
   SafeACK = (SND.UNA advances and no further loss indicated)
   if (inflight > ssthresh) {
      // Proportional Rate Reduction
      // This uses integer division, rounding up:
      #define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
      out = DIV_ROUND_UP(prr_delivered * ssthresh, RecoverFS)
      SndCnt = out - prr_out
   } else {
      // PRR-CRB by default
      SndCnt = MAX(prr_delivered - prr_out, DeliveredData)
      if (SafeACK) {
         // PRR-SSRB when recovery is making good progress
         SndCnt += SMSS
      }
      // Attempt to catch up, as permitted
      SndCnt = MIN(ssthresh - inflight, SndCnt)
   }


   if (prr_out is 0 AND SndCnt is 0) {
      // Force a fast retransmit upon entering recovery
      SndCnt = SMSS
   }
   cwnd = inflight + SndCnt

Once the sender has worked out SndCnt and used it to update cwnd, it goes ahead and transmits more data ๐Ÿš€ Which data gets sent (retransmitting missing stuff or sending new data) is out of scope for this document, so that's a different conversation ๐Ÿ’…

6.3. Per-Transmit Steps ๐Ÿ“

Every time data is transmitted or retransmitted, PRR runs this one line:

   prr_out += (data sent)

6.4. Completion Steps ๐ŸŒˆ

A PRR episode is over either once fast recovery finishes, or right before a brand new PRR episode starts because of a new congestion control response episode ๐Ÿ‘‘

When a PRR episode completes, PRR does this:

   cwnd = ssthresh

Heads up, this step that sets cwnd to ssthresh can, in some situations, let a burst of back-to-back segments hit the network all at once ๐Ÿ˜ญ

It is RECOMMENDED that implementations use pacing to smooth out the burstiness of data traffic ๐Ÿซถ This lines up with what people already do to tame bursts (see [PACING]), including pacing transmission bursts after restarting from idle โœจ

7. Properties ๐Ÿ’–

okay so these properties are shared by PRR-CRB and PRR-SSRB, except where I say otherwise ๐ŸŽ€

PRR tries to keep the sender's ACK clocking going through recovery, even when there are burst losses ๐Ÿฆ‹ [RFC6675] can send big, unclocked bursts after burst losses, which is kind of chaotic ๐Ÿ’€

Normally PRR spreads voluntary window reductions evenly across a full RTT โœจ This can make Internet traffic less bursty, and you could think of it as a type of soft pacing ๐ŸŒธ Hypothetically, any pacing makes it more likely that different flows get interleaved, so there's less chance of ACK compression and other things that make traffic burstier ๐Ÿซถ These effects have not been quantified though, so don't take that as settled ๐Ÿ™ˆ

If there are minimal losses, PRR converges to exactly the target window that the congestion control algorithm picked ๐Ÿ‘‘ As the sender gets close to the end of recovery, prr_delivered approaches RecoverFS, and SndCnt gets computed so that prr_out approaches ssthresh ๐Ÿงš

Implicit window reductions happen when there are multiple isolated losses during recovery, and they make later voluntary reductions get skipped ๐Ÿ’… For small numbers of losses, the window size ends up at exactly the window the congestion control algorithm chose โœจ

For burst losses, earlier voluntary window reductions can be undone by sending extra segments in response to ACKs that show up later during recovery ๐Ÿ˜ญ As long as some voluntary window reductions aren't undone, and there's no application stall, the final value for inflight will be the same as ssthresh ๐Ÿ“

PRR with either Reduction Bound makes things better when there are application stalls ๐Ÿ”ฅ That's when the sending application doesn't queue data for transmission fast enough, or the receiver stops advancing its receive window ๐Ÿ™ˆ If there's an application stall early in recovery, prr_out falls behind the sum of transmissions allowed by SndCnt ๐Ÿ˜ญ The missed chances to send because of stalls get treated like banked voluntary window reductions, so they make prr_delivered - prr_out significantly positive ๐Ÿ’œ If the application catches up while the sender is still in recovery, the sender sends a partial window burst to grow inflight back to exactly where it would have been if the application had never stalled ๐ŸŒˆ Yes, that burst could hurt the given flow or other flows sharing the path, but this is exactly what happens every time there's a partial-RTT application stall while not in recovery ๐Ÿ’€ PRR makes partial-RTT stall behavior uniform in all states โœจ Changing this behavior is out of scope for this document ๐Ÿ™ƒ

PRR with Reduction Bound is less sensitive to errors in the inflight estimator ๐Ÿฆ‹ While in recovery, inflight is basically an estimator, since it uses incomplete information to guess whether un-SACKed segments are really lost or just out of order in the network ๐Ÿง Under some conditions inflight can be pretty wrong. For example, inflight is underestimated when a burst of reordered data is prematurely assumed to be lost and marked for retransmission ๐Ÿ˜ฑ If transmissions are regulated directly by inflight, like they are with [RFC6675], a step discontinuity in the inflight estimator causes a burst of data that can't be taken back once the estimator gets corrected a few ACKs later ๐Ÿ’… For PRR dynamics, inflight only decides which algorithm, PRR or the Reduction Bound, gets used to compute SndCnt from DeliveredData โœจ While inflight is underestimated, the two algorithms differ by at most 1 segment per ACK ๐ŸŽ€ Once inflight gets updated, they converge to the same final window at the end of recovery ๐Ÿ‘‘

Under all conditions and sequences of events during recovery, PRR-CRB strictly bounds the data transmitted to be equal to or less than the amount of data delivered to the receiver ๐Ÿ”ฅ This is the Strong Packet Conservation Bound, and it's the most aggressive algorithm that doesn't lead to additional forced losses in some environments ๐Ÿ˜ค It has the property that if there's a standing queue at a bottleneck with no cross traffic, the queue stays at exactly constant length for the whole recovery, except for +1/-1 fluctuation caused by differences in packet arrival and exit times ๐ŸŒธ See Appendix A for a detailed discussion of this property ๐Ÿ“š

The Strong Packet Conservation Bound is very appealing for a bunch of reasons ๐Ÿ’– But earlier measurements (in Section 6 of [RFC6675]) show that it's less aggressive and doesn't perform as well as [RFC6675], which permits bursts of data when there are bursts of losses ๐Ÿ˜ฌ PRR-SSRB is a compromise. It lets a sender send one extra segment per ACK compared to the Packet Conserving Bound when the ACK shows that recovery is making good progress without further losses ๐Ÿฆ‹ From the perspective of a strict Packet Conserving Bound, PRR-SSRB does open the window during recovery, yes ๐Ÿ˜ณ But it's significantly less aggressive than [RFC6675] when there are burst losses ๐Ÿซถ


8. Examples ๐Ÿงš

this section shows how the PRR and [RFC6675] algorithms behave differently in two example scenarios: a connection that has either a single loss, or a burst of 15 consecutive losses ๐ŸŽ€ All cases use bulk data transfers (no application pauses), Reno congestion control [RFC5681], and cwnd = FlightSize = inflight = 20 segments, so ssthresh gets set to 10 at the beginning of recovery ๐Ÿ’… The scenarios use standard Fast Retransmit [RFC5681] and Limited Transmit [RFC3042], so the sender sends two new segments followed by one retransmit in response to the first three duplicate ACKs after the losses โœจ

Each of the diagrams below shows the per-ACK response to the first round trip for the two recovery algorithms when the zeroth segment is lost ๐Ÿฆ‹ The top line ("ack#") shows the transmitted segment number that triggers the ACKs, with an X for the lost segment ๐Ÿ’œ The "cwnd" and "inflight" lines show the values of cwnd and inflight for these algorithms after each returning ACK is processed, but before any further (re)transmission ๐ŸŒˆ The "sent" line shows how much "N"ew or "R"etransmitted data would be sent ๐Ÿ“ Just a heads up: the algorithms for deciding which data to send are out of scope for this document ๐Ÿ™ƒ

RFC 6675 and PRR, first example (single round trip, zeroth segment lost):

   RFC 6675
   a X  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22
   c   20 20 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
   i   19 19 18 18 17 16 15 14 13 12 11 10  9  9  9  9  9  9  9  9  9  9
   s    N  N  R                             N  N  N  N  N  N  N  N  N  N








   PRR
   a X  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22
   c   20 20 19 18 18 17 17 16 16 15 15 14 14 13 13 12 12 11 11 10 10 10
   i   19 19 18 18 17 17 16 16 15 15 14 14 13 13 12 12 11 11 10 10  9  9
   s    N  N  R     N     N     N     N     N     N     N     N     N  N


   a: ack#;  c: cwnd;  i: inflight;  s: sent

Figure 1

in this first example, ACK#1 through ACK#19 contain SACKs for the original flight of data ๐ŸŒธ ACK#20 and ACK#21 carry SACKs for the limited transmits triggered by the first and second SACKed segments ๐ŸŽ€ ACK#22 carries the full cumulative ACK that covers all data up through the limited transmits ๐Ÿ‘‘ ACK#22 completes the fast recovery episode, and that also completes the PRR episode โœจ

Both algorithms send the same total amount of data, and both finish the fast recovery episode with a cwnd matching the ssthresh of 20 ๐Ÿ’– [RFC6675] goes through a "half window of silence", while PRR spreads the voluntary window reduction across an entire RTT ๐Ÿฆ‹

now for the next example ๐Ÿ˜ค It has the same initial conditions, except the first 15 packets (0-14) are lost ๐Ÿ’€ During the rest of the lossy round trip, only 5 ACKs make it back to the sender ๐Ÿ™ˆ The following looks at each of these algorithms, one after the other ๐Ÿงš

RFC 6675 and PRR, second example (first 15 packets lost):

   RFC 6675
   a X  X  X  X  X  X  X  X  X  X  X  X  X  X  X  15 16 17 18 19
   c                                              20 20 10 10 10
   i                                              19 19  4  9  9
   s                                               N  N 6R  R  R




   PRR
   a X  X  X  X  X  X  X  X  X  X  X  X  X  X  X  15 16 17 18 19
   c                                              20 20  5  5  5
   i                                              19 19  4  4  4
   s                                               N  N  R  R  R


   a: ack#;  c: cwnd;  i: inflight;  s: sent

Figure 2

in this specific situation, [RFC6675] is more aggressive ๐Ÿ˜ฌ Once Fast Retransmit is triggered (on the ACK for segment 17), the sender immediately retransmits enough data to bring inflight up to cwnd ๐Ÿ”ฅ Earlier measurements (in Section 6 of [RFC6675]) indicate that [RFC6675] significantly outperforms the [RFC6937] version of PRR, which only uses PRR-CRB and some other similarly conservative algorithms that were tested ๐ŸŒˆ That shows it's pretty common for the actual losses to exceed the cwnd reduction determined by the congestion control algorithm ๐Ÿ’œ

Under such heavy losses, during the first round trip of fast recovery, PRR uses PRR-CRB to follow the packet conservation principle ๐Ÿซถ Since the total losses bring inflight below ssthresh, data is sent so that the total data transmitted, prr_out, follows the total data delivered to the receiver as reported by returning ACKs โœจ Transmission is controlled by the sending limit, which is set to prr_delivered - prr_out ๐ŸŽ€

Not shown in the figure above: once the fast retransmits sent starting at ACK#17 are delivered and trigger ACKs that increment SND.UNA, PRR enters PRR-SSRB ๐Ÿฆ‹ It then increases the window by exactly 1 segment per ACK until inflight rises to ssthresh during recovery ๐ŸŒธ On heavy losses when cwnd is large, PRR-SSRB recovers the losses exponentially faster than PRR-CRB ๐Ÿ˜ณ Increasing the window during recovery might seem ill advised, but remember that this is actually less aggressive than what [RFC6675] allows ๐Ÿ’… [RFC6675] sends the same quantity of additional data as a single burst in response to the ACK that triggered Fast Retransmit ๐Ÿ’€

For less severe loss events, where the total losses are smaller than the difference between FlightSize and ssthresh, PRR-CRB and PRR-SSRB aren't invoked, since PRR stays in the Proportional Rate Reduction mode ๐Ÿงš


9. Adapting PRR to Other Transport Protocols ๐ŸŒˆ

the main PRR algorithm and reduction bounds can be adapted to any transport that can support [RFC6675] ๐ŸŽ€ In one major implementation (Linux TCP), PRR has been the fast recovery algorithm for its default and supported congestion control modules since it was introduced in 2011 [First_TCP_PRR] ๐Ÿ‘‘

The SafeACK heuristic can be generalized as any ACK of a retransmission that doesn't cause some other segment to be marked for retransmission โœจ


10. Measurement Studies ๐Ÿ“š

for [RFC6937], a companion paper [IMC11] evaluated [RFC3517] and various experimental PRR versions in a large-scale measurement study ๐Ÿฆ‹ At the time of publication, the legacy algorithms used in that study are no longer in the code base that was used, which makes those comparisons hard without recreating the historical algorithms ๐Ÿ˜ญ If you're interested in the measurement study, you should check out Section 5 of [RFC6937] and the IMC paper [IMC11] ๐Ÿซถ


11. Operational Considerations ๐Ÿ“

11.1. Incremental Deployment ๐ŸŽ€

okay so PRR is super easy to roll out ๐Ÿ’– bc it only uses transport protocol mechanisms that already exist for acknowledging delivered data and detecting lost data โœจ

PRR only requires changes to the transport protocol implementation at the data sender ๐Ÿซถ it does not require any changes at data receivers or in networks. this allows data senders using PRR to work correctly with any existing data receivers or networks ๐ŸŒˆ PRR does not require any changes to or assistance from routers, switches, or other devices in the network. no middlebox drama here ๐Ÿ’…

11.2. Fairness ๐Ÿฆ‹

PRR is designed to maintain the fairness properties of the congestion control algorithm it's deployed with โš–๏ธ PRR only operates during a congestion control response episode, such as fast recovery or when there is a step reduction in the cwnd from the TCP ECN reaction defined in [RFC3168]. it only makes short-term, per-acknowledgment decisions to smoothly regulate the volume of in-flight data during an episode, so that at the end of the episode it will be as close as possible to the slow start threshold (ssthresh), as determined by the congestion control algorithm ๐ŸŒธ

PRR does not modify the congestion control cwnd increase or decrease mechanisms outside of congestion control response episodes. it stays in its lane ๐Ÿงš

11.3. Protecting the Network Against Excessive Queuing and Packet Loss ๐ŸŒŠ

over long time scales, PRR is designed to maintain the queuing and packet loss properties of the congestion control algorithm it's deployed with ๐Ÿ’œ as noted above, PRR only operates during a congestion control response episode, such as fast recovery or response to ECN. it only makes short-term, per-acknowledgment decisions to smoothly regulate the volume of in-flight data during an episode, so that at the end of the episode it will be as close as possible to the slow start threshold (ssthresh), as determined by the congestion control algorithm.

over short time scales, PRR is designed to cause lower packet loss rates than preceding approaches like [RFC6675] ๐Ÿ˜ญ at a high level, PRR is inspired by the packet conservation principle, and as much as possible, PRR relies on the self clock process ๐Ÿ”ฅ by contrast, with [RFC6675], a single ACK carrying a SACK option that implies a large quantity of missing data can cause a step discontinuity in the pipe estimator. that can cause Fast Retransmit to send a large burst of data that is much larger than the volume of delivered data ๐Ÿ’€

PRR avoids such bursts by basing transmission decisions on the volume of delivered data rather than the volume of lost data โœจ furthermore, as noted above, PRR-SSRB is less aggressive than [RFC6675] (transmitting fewer segments or taking more time to transmit them), and it outperforms due to the lower probability of additional losses during recovery ๐Ÿ‘‘


12. IANA Considerations ๐ŸŒท

This document has no IANA actions.

13. Security Considerations ๐Ÿšจ

PRR does not change the risk profile for transport protocols. the vibe is stable ๐Ÿซถ

Implementers that change PRR from counting bytes to segments have to be cautious about the effects of ACK splitting attacks [Savage99], where the receiver acknowledges partial segments for the purpose of confusing the sender's congestion accounting. this is a real threat, so don't skip this part bestie ๐Ÿ˜ฑ

14. References ๐Ÿ“š

14.1. Normative References ๐Ÿ’–

14.2. Informative References ๐Ÿง 

Appendix A. Strong Packet Conservation Bound โœจ

okay so PRR-CRB is built on this super conservative, philosophically pure, and honestly aesthetically gorgeous ๐Ÿฆ‹ thing called the Strong Packet Conservation Bound. It's inspired by the packet conservation principle [Jacobson88], but it handles segments that are missing and presumed lost in its own way ๐Ÿ’…. Under every condition and every sequence of events during recovery, PRR-CRB strictly bounds the data transmitted to be equal to or less than the amount of data delivered to the receiver ๐Ÿ”ฅ. Also, the effects of presumed losses get counted in the inflight calculation, but they don't change the outcome of PRR-CRB once inflight has dropped below ssthresh ๐Ÿ‘‘.

This Strong Packet Conservation Bound is the most aggressive algorithm that does not lead to additional forced losses in some environments ๐Ÿ˜ญ. Here's the vibe: if there's a standing queue at a bottleneck that isn't carrying any other traffic, that queue stays at exactly constant length for the whole recovery, apart from a +1/-1 wobble caused by differences in when packets arrive and when they leave ๐ŸŒˆ. A less aggressive algorithm makes the queue shrink. A more aggressive one makes it grow, or, if it's a full drop tail queue, causes extra losses ๐Ÿ’€.

Let's check this with a thought experiment ๐Ÿงš:

Picture a network path where the delays in both directions are insignificant, except for the processing time and the queue at one bottleneck in the forward path. When a packet gets "served" at the head of that bottleneck queue, all of these things happen in much less than one bottleneck packet time: the packet arrives at the receiver; the receiver sends an ACK that arrives at the sender; the sender processes the ACK and sends some data; and that data gets queued at the bottleneck ๐Ÿฆ‹.

Now, if SndCnt is set to DeliveredData and nothing else is stopping data from being sent, then the data arriving at the bottleneck queue will exactly replace the data that was served at the head of the queue, so the queue has a constant length ๐ŸŽ€. If the queue is drop tail and full, it stays exactly full. Losses or reordering on the ACK path only make the queue size fluctuate more widely. They don't raise its peak size, whether the data is in order or out of order (including loss recovery from an earlier RTT) ๐Ÿซถ. Any more aggressive algorithm that sends additional data overflows the drop tail queue and causes loss, and any less aggressive algorithm under-fills the queue ๐Ÿ’œ. So setting SndCnt to DeliveredData is the most aggressive algorithm that doesn't cause forced losses in this simple network ๐Ÿ“. Relaxing the assumptions (for example, making delays more authentic, adding more flows, adding delayed ACKs, and so on) is likely to increase the fine-grained fluctuations in queue size, but it doesn't change the basic behavior โœจ.

One more thing, bestie: the congestion control algorithm has a broader idea of "optimal" that includes fairly sharing the network ๐Ÿ‘‘. Typical congestion control algorithms are likely to send less data than the Packet Conserving Bound that PRR implements, bringing TCP's actual window down to ssthresh ๐ŸŒธ.


Acknowledgments ๐Ÿซถ

This document is based in part on earlier work by Janey C. Hoe (see "Recovery from Multiple Packet Losses", Section 3.2 of [Hoe96Startup]), Matt Mathis, Jeff Semke, and Jamshid Mahdavi [TCP-RH], and it was influenced by several discussions with John Heffner ๐Ÿ’–.

Monia Ghobadi and Sivasankar Radhakrishnan helped analyze the experiments ๐Ÿ”ฅ. Ilpo Jarvinen reviewed the initial implementation ๐Ÿงš. Mark Allman, Richard Scheffenegger, Markku Kojo, Mirja Kuehlewind, Gorry Fairhurst, Russ Housley, Paul Aitken, Daniele Ceccarelli, and Mohamed Boucadair made the document better with their insightful reviews and suggestions ๐ŸŒˆ.


Authors' Addresses ๐ŸŽ€

Matt Mathis Email: matt.mathis@gmail.com ๐Ÿ’œ

Neal Cardwell Google, Inc. Email: ncardwell@google.com ๐Ÿฆ‹

Yuchung Cheng Google, Inc. Email: ycheng@google.com ๐ŸŒธ

Nandita Dukkipati Google, Inc. Email: nanditad@google.com โœจ

๐Ÿ“ฆ where this page comes from