Skip to content

Add new data channel high / low water mark approach for packet congestion control#2013

Open
1egoman wants to merge 4 commits into
mainfrom
data-channel-buffer-range
Open

Add new data channel high / low water mark approach for packet congestion control#2013
1egoman wants to merge 4 commits into
mainfrom
data-channel-buffer-range

Conversation

@1egoman

@1egoman 1egoman commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

TODO: add notes in here

@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2b509cd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
livekit-client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
dist/livekit-client.esm.mjs 101.53 KB (+0.05% 🔺)
dist/livekit-client.umd.js 110.56 KB (+0.18% 🔺)

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread src/room/RTCEngine.ts Outdated
Comment thread src/room/RTCEngine.ts Outdated
Comment thread src/room/RTCEngine.ts
Comment on lines +1796 to +1804
await new TypedPromise<void, UnexpectedConnectionState>((resolve, reject) => {
const onBufferedAmountLow = () => resolve();
dc.addEventListener('bufferedamountlow', onBufferedAmountLow, { once: true });
// Proxy along any error caused by the engine closing while we wait.
this.bufferStatusLowClosingFuture.promise.catch((e) => {
dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
reject(e);
});
});

@devin-ai-integration devin-ai-integration Bot Jul 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Flow-control wait can deadlock all senders if the data channel closes unexpectedly without the engine closing

The per-kind mutex is held while awaiting the browser's drain event (await lock.lock() at src/room/RTCEngine.ts:1784), but if the data channel closes independently of the engine, the drain event never fires and the mutex is never released, so all subsequent sends for that channel kind hang forever.

Impact: If a data channel is aborted (e.g. by an oversized message), all future reliable or data-track sends silently hang instead of failing with an error.

Trigger condition and mechanism

The waitUntilBelowHighWaterMark method (line 1778) acquires a per-kind mutex, then if the buffer is above the high-water mark, awaits a bufferedamountlow event on the data channel (line 1798). The only escape hatch is bufferStatusLowClosingFuture which rejects only when the engine emits EngineEvent.Closing (src/room/RTCEngine.ts:302).

However, handleDataChannelClose (src/room/RTCEngine.ts:1107-1117) only logs an error — it does not reject the future or release the mutex. So if the DC closes while a sender is waiting (e.g. because an oversized message aborted the SCTP association), the TypedPromise at line 1796 never resolves or rejects. The finally block (line 1805) never runs, so unlock() is never called. All subsequent callers of waitUntilBelowHighWaterMark for that kind will block on lock.lock() at line 1784 indefinitely.

Before this PR, a single caller would hang but other callers could still attempt sends. The mutex serialization introduced by this PR turns a single-caller hang into a full deadlock for all callers of that kind.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@1egoman 1egoman force-pushed the data-channel-buffer-range branch from a34512a to 2b509cd Compare July 15, 2026 15:27

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread src/room/RTCEngine.ts
const dc = this.dataChannelForKind(kind);
if (dc) {
if (!this.isBufferStatusLow(kind)) {
if (!this.isBelowHighWaterMark(kind)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Lossy data channel drops messages too late, breaking adaptive latency control

The lossy channel's drop-or-send decision now compares against a fixed 256 KB ceiling (isBelowHighWaterMark at src/room/RTCEngine.ts:1676) instead of the adaptive threshold that targeted ~100 ms of buffered latency, so low-rate lossy streams can accumulate seconds of data before any messages are dropped.

Impact: Lossy data (e.g. real-time sensor telemetry) may experience significantly higher latency at low data rates because the buffer must fill to 256 KB before dropping kicks in, whereas the old adaptive threshold could be as low as 8 KB.

Mechanism: adaptive threshold is computed but no longer consulted for the send gate

The adaptive threshold is still computed every second and written to dc.bufferedAmountLowThreshold (src/room/RTCEngine.ts:977-980), but the send gate in sendLossyBytes now calls isBelowHighWaterMark(kind) (src/room/RTCEngine.ts:1676) which compares dc.bufferedAmount against the fixed dataChannelHighWaterMark(DataChannelKind.LOSSY) = 256 KB (src/room/RTCEngine.ts:122). Previously, isBufferStatusLow compared against dc.bufferedAmountLowThreshold, which was the adaptive value clamped between 8 KB and 256 KB based on the measured byte rate. The comment at src/room/RTCEngine.ts:141-142 even states the lossy channel "keeps its adaptive single threshold and never consults these" watermarks, confirming the code contradicts the intended design.

Prompt for agents
In sendLossyBytes, the drop decision for the lossy channel (DataChannelKind.LOSSY) should use the adaptive bufferedAmountLowThreshold rather than the fixed high-water mark. The old code checked dc.bufferedAmount <= dc.bufferedAmountLowThreshold which respected the adaptive threshold computed every second in the setInterval at line 969. The new isBelowHighWaterMark function compares against the fixed 256KB constant, which defeats the adaptive latency control. Consider either: (1) restoring the old adaptive check for LOSSY kind specifically (e.g. keep using dc.bufferedAmountLowThreshold for LOSSY), or (2) using isBelowHighWaterMark only for RELIABLE and DATA_TRACK_LOSSY kinds. The same issue applies to the updateAndEmitDCBufferStatus function at line 1729 which now uses isBelowLowWaterMark (fixed 8KB) instead of the adaptive threshold for the DCBufferStatusChanged event.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant