Repository navigation
Conversation
Without a cc option the lsquic engines built for listen() and connect() were left on lsquic's adaptive default, which commits each connection to Cubic or BBRv1 from its first RTT sample. Node defaults to cubic, and the adaptive pick is the wrong tool for an engine that is only ticked from the event loop: the RTT sample measures the JS that ran between ticks, and once BBRv1 is picked it models that loop latency as the path and shrinks the window to its 4-packet floor on a busy loop. Set Cubic unless cc: 'bbr' is asked for; 'reno' keeps mapping to Cubic since lsquic ships no Reno.
|
Warning Review limit reached
Next review available in: 7 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Status Reproduced with the released binary (1.4.0): Fix: |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not a duplicate of #37455: that PR changes the two engines in packages/bun-usockets/src/quic.c (Bun.serve HTTP/3 and the fetch client) and says it leaves node:quic alone. This one is the node:quic half, in src/runtime/node/quic/endpoint.rs where the listen()/connect() engines are configured and where the user-facing cc option is mapped. The two do not overlap in code and can land in either order. |
There was a problem hiding this comment.
LGTM — one-line default in apply_transport_params aligns node:quic with Node's documented cc: 'cubic' default, mirroring #37455's fix for the other engines.
What was reviewed:
CC_ALGO_CUBIC=1/CC_ALGO_BBR=2match lsquic'ses_cc_algoenum; the default is set before the!options.is_object()early return so both paths get it.- The catch-all arm now maps to Cubic instead of 0 (adaptive) — unreachable through the JS layer's
validateOneOf, but safer. - Fixture wires early process exit to reject the awaited promises, uses
await usingfor cleanup, and the negativecc: 'bbr'test proves the fixture actually detects the collapse the other two tests rule out.
Extended reasoning...
Overview
The PR changes apply_transport_params in src/runtime/node/quic/endpoint.rs to set es_cc_algo to Cubic (1) before parsing options, so a session created without cc gets Node's documented default instead of lsquic's "adaptive" mode. The explicit-cc match is simplified: bbr → BBRv1, everything else → Cubic (previously the catch-all mapped to 0, which is adaptive again). Two named constants replace the raw integers. The rest of the diff is a new test block in quic-endpoint.test.ts plus a subprocess fixture that reproduces BBRv1's cwnd collapse on a busy event loop.
Security risks
None. This changes a congestion-control default on an experimental node:quic module; no auth, crypto, parsing, or memory-ownership changes. The setter (us_nq_settings_set_cc_algo) already existed and is called with compile-time constants.
Level of scrutiny
Low-to-medium. The runtime change is effectively one line (s.cc_algo(CC_ALGO_CUBIC)) placed before the existing early-return, plus a defensive tightening of an already-validated match arm. It is the node:quic companion to #37455, which made the same change for Bun.serve({http3})/fetch. The constant values (1=Cubic, 2=BBRv1, 3=Adaptive default) line up with lsquic's es_cc_algo documentation and with the pre-existing comment this PR replaces.
Other factors
The test is timing-oriented by necessity — it is measuring congestion-controller behavior — but is designed against flakiness: it takes the minimum cwnd over the last 30 of 60 iterations (tolerating BBR's ACK-bunching bumps), the 32 KiB threshold sits well between the observed BBR floor (5840) and Cubic's 100+ KiB, and a third test with cc: 'bbr' asserts the collapse is observed by the same fixture. The PR description reports 10/10 failure on USE_SYSTEM_BUN=1 and 16/16 pass on the debug build, plus passing upstream Node QUIC tests. The subprocess helper wires proc.exited into a rejection so a fixture crash fails fast rather than hanging, and resources are released with await using. Bun.sleepSync here is the mechanism under test (a busy loop between engine ticks), not a wait-for-condition, and each site is commented per the review guidance.
…, drop the bbr control The fixed idle delay did not always give BBR a loopback RTT sample on a loaded machine, and the explicit cc: 'bbr' run does not always reach the floor within the window when starved to a single core, so it is no longer asserted on. The healthy threshold moves to 16 KiB so that it also clears Cubic's settling point behind Windows' 64 KiB default UDP buffer.
|
Updated 4:24 AM PT - Aug 11th, 2026
✅ @robobun, your commit a588120d5593ba373dbc150f29948c51f41edb70 passed in 🧪 To try this PR locally: bunx bun-pr 37468That installs a local version of the PR into your bun-37468 --bun |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. The Rust change is small and correct (Cubic default set ahead of option parsing, named constants match the lsquic es_cc_algo values, and the _ => 0 adaptive fall-through is gone). Because the new tests assert on congestion-window behaviour under a busy event loop — timing-sensitive by nature, even with the 3× margin and the condition-based free-run phase — a human look at the fixture's CI stability across platforms would still be worthwhile.
What was reviewed:
apply_transport_params: defaultcc_algo(CC_ALGO_CUBIC)mirrors the existingidle_timeout(10)pattern and covers the!options.is_object()early-return path.ccmapping:bbr→ 2, everything else → 1; the JS layer restrictsccto'reno'|'cubic'|'bbr', so no value silently changes meaning.- Fixture: free-run awaits
bytesSent(not a fixed sleep),diedrace prevents hangs on fixture crash, resources released viaawait using, gracefulsession.close()lets both sides finish.
Extended reasoning...
Overview
Three files: src/runtime/node/quic/endpoint.rs (+7/−8 net), a new 113-line test fixture busy-loop-sender-fixture.ts, and ~110 new lines in quic-endpoint.test.ts. The runtime change sets lsquic's es_cc_algo to Cubic before reading options (so both the no-options path and the cc-unset path get it), replaces magic numbers with CC_ALGO_CUBIC/CC_ALGO_BBR constants, and drops the _ => 0 (adaptive) catch-all. This is the node:quic half of the same fix #37455 applies to the usockets HTTP/3 engines.
Security risks
None identified. The change only picks a congestion-control algorithm; it doesn't touch TLS, certificate handling, or input validation. The cc option was already validated in the JS layer before reaching this match.
Level of scrutiny
Moderate. The Rust diff is trivial and follows the exact pattern used two lines above for idle_timeout. What warrants a human look is the test: it spawns a fixture that blocks the thread for 30 ms during handshake, free-runs until 256 KiB are ACKed, then paces 60 iterations of 12 ms sleepSync and asserts the minimum stats.cwnd over the last 30 stays ≥ 16 KiB. The author stress-tested this (10/10 runs, 6/6 with three concurrent copies, 6/6 pinned to 1–2 CPUs) and the 16 KiB threshold sits ~3× below Cubic's smallest observed floor (≈50 KiB on Windows' 64 KiB UDP buffer), but congestion-control behaviour on shared CI runners across linux/macos/windows is inherently variable and worth a maintainer's judgement call.
Other factors
- The comment-cop bot flagged long comments five times; the author iterated down to one-line docs (all threads resolved).
describe.concurrentkeeps the two ~4 s tests overlapping; the 30 s timeout is justified by debug-buildnode:quicload time.spawnSenderraces IPC againstproc.exitedso a crashed fixture fails the test with its stderr instead of hanging — good pattern.- The
h3blast.ccommentes_cc_algo = 3; // BBRv1disagrees with this PR's (and the pre-existing endpoint.rs comment's) reading of 3 = Adaptive; that file isn't touched here and its comment is likely just stale, but noting it in case #37455's review didn't already. - This changes a user-visible default in a Node compat module; aligning with Node's cubic default is the right call, but a human should confirm they're happy landing this independently of #37455.
Problem
A
node:quicsession created without theccoption gets lsquic's default congestion controller, which ises_cc_algo3, "adaptive". Node's default iscubic(ngtcp2's default;src/js/internal/quic/quic.tsonly validatesccand passesundefinedthrough, andapply_transport_paramsinsrc/runtime/node/quic/endpoint.rsonly calledcc_algo()when the option was given).Adaptive mode is also the wrong default for these engines on its own merits. It commits each connection to Cubic or BBRv1 on the connection's first RTT sample (
send_ctl_select_cc, threshold 1.5 ms), and bothnode:quicengines are only ticked from the event loop, so that sample measures whatever JS ran between two ticks. A server that does any work in its session callback, or a client that does any work right afterconnect(), gets BBRv1 (BUN_DEBUG_lsquic=1:select BBRv1 congestion controller), and BBRv1 then keeps modelling the loop as the path: the only bandwidth it can observe is one window per loop iteration while its min RTT is the real loopback RTT, so on a busy loop the window shrinks to BBR's 4-packet floor and stays there. With the released binary, a session sending 512 KiB chunks from a loop paced at 12 ms per iteration reportssession.stats.cwnd === 5840nwithin 20 iterations (20/20 runs) and moves about 5.9 KB per iteration from then on; the same transfer on Cubic keeps a window of 100 to 240 KiB (bounded by the receiver's UDP buffer) and moves 4 to 170 times as much. Which of the two a given session got depended on what the loop happened to be doing during its handshake. #37455 makes the same change for theBun.serve({http3})andfetchengines inquic.cand deliberately leftnode:quicout; this is thenode:quichalf.Fix
apply_transport_paramssets Cubic before looking at the options, so the default and theoptionsis-not-an-object path both get it, and theccmapping is nowbbrto BBRv1 and everything else (cubic, andreno, which lsquic does not ship) to Cubic. The previous catch-all arm mapped to 0, which is adaptive again; nothing reaches it through the public API (the JS layer validatescc), but it is gone too. Both engines go through this function (build_engine, fromlisten()andconnect()).Verification
test/js/node/quic/quic-endpoint.test.ts, "congestion control default": one test per engine (thelisten()one sends to the test's client, theconnect()one sends to the test's server), with the busy sender intest/js/node/quic/busy-loop-sender-fixture.ts. Two things about the fixture are worth calling out:listen()session callback, and right afterconnect(), which has already put the Initial on the wire), which makes that sample about 30 ms and the pick BBRv1 every time; with the fix there is no pick to make.stats.cwndit saw over the last 30 of them. The tests require 16 KiB: Cubic settles just under what the receiver's UDP buffer holds (100 KiB and up with Linux's 208 KiB default, about 50 to 64 KiB behind Windows' 64 KiB default), and the unfixed default reports exactly 5840.Results:
USE_SYSTEM_BUN=1, 1.4.0): both tests fail withReceived: 5840in 10/10 runs of the file, and in 3/3 runs with the whole thing pinned to one CPU.node:quicin a debug build (hence the explicit timeout).test/js/node/quic/passes, and the upstreamtest-quic-cc-algorithm,-session-stats-detailed,-stream-bidi-varchunklen,-flow-control-uni,-datagramand-connection-concurrenttests pass with their flags.cc: 'bbr'run of the fixture on the fixed build reports the same 5840, which is how the fixture was checked against the thing it rules out; it is not asserted on in the suite because, starved to a single core, BBR did not always get all the way down within the window (one run out of three reported 20 KiB).