Skip to content

Low bandwidth compatible pacer #2286

@vitocln

Description

@vitocln

We are experimenting with using Quinn over slow internet connections, such as 400 Kbps. We've observed unexpected and continuous packet losses in this context. After some investigation, we've identified the pacer as the primary cause.

The current pacer is designed for typical internet connections and enforces a hard lower bound of 10 packets per 2 ms. This rate is excessive for low-bandwidth networks.

To address this, we've implemented a "dirty fix" that allows for reduced pacing capacity and slower token refill rates. With this patch, the unexpected packet losses were eliminated:

diff --git a/quinn-proto/src/connection/pacing.rs b/quinn-proto/src/connection/pacing.rs
index 2e469948..da2dbcf0 100644
--- a/quinn-proto/src/connection/pacing.rs
+++ b/quinn-proto/src/connection/pacing.rs
@@ -28,7 +28,7 @@ impl Pacer {
             capacity,
             last_window: window,
             last_mtu: mtu,
-            tokens: capacity,
+            tokens: capacity.max(mtu as u64), // build up the first token directly
             prev: now,
         }
     }
@@ -89,11 +89,11 @@ impl Pacer {
         }
 
         let elapsed_rtts = time_elapsed.as_secs_f64() / smoothed_rtt.as_secs_f64();
-        let new_tokens = window as f64 * 1.25 * elapsed_rtts;
+        let new_tokens = (window as f64 * 1.25 * elapsed_rtts).min(self.capacity as f64);
+
         self.tokens = self
             .tokens
-            .saturating_add(new_tokens as _)
-            .min(self.capacity);
+            .saturating_add(new_tokens as _);
 
         self.prev = now;
 
@@ -103,7 +103,7 @@ impl Pacer {
         }
 
         let unscaled_delay = smoothed_rtt
-            .checked_mul((bytes_to_send.max(self.capacity) - self.tokens) as _)
+            .checked_mul((bytes_to_send.max(self.capacity) - self.tokens).min(self.capacity) as _)
             .unwrap_or(Duration::MAX)
             / window;
 
@@ -131,9 +131,7 @@ fn optimal_capacity(smoothed_rtt: Duration, window: u64, mtu: u16) -> u64 {
 
     let capacity = ((window as u128 * BURST_INTERVAL_NANOS) / rtt) as u64;
 
-    // Small bursts are less efficient (no GSO), could increase latency and don't effectively
-    // use the channel's buffer capacity. Large bursts might block the connection on sending.
-    capacity.clamp(MIN_BURST_SIZE * mtu as u64, MAX_BURST_SIZE * mtu as u64)
+    capacity.min(MAX_BURST_SIZE * mtu as u64)
 }
 
 /// The burst interval
@@ -144,8 +142,7 @@ fn optimal_capacity(smoothed_rtt: Duration, window: u64, mtu: u16) -> u64 {
 /// more applicable.
 const BURST_INTERVAL_NANOS: u128 = 2_000_000; // 2ms
 
-/// Allows some usage of GSO, and doesn't slow down the handshake.
-const MIN_BURST_SIZE: u64 = 10;
+const MIN_BURST_SIZE: u64 = 1;
 
 /// Creating 256 packets took 1ms in a benchmark, so larger bursts don't make sense.
 const MAX_BURST_SIZE: u64 = 256;

However, this approach has a trade-off: Quinn's transmit loop is now triggered and blocked by the pacer more frequently, leading to increased CPU usage.

We understand that this patch is not ready to be merged upstream in its current form. However, if there's interest, we're willing to invest additional time to rework it into a cleaner solution — one that supports both regular and low-bandwidth networks. For example, we could introduce a TransportConfig option to enable reduced pacer capacity where appropriate.

Would the Quinn project be interested in supporting a pacer compatible with slow network conditions?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions