Skip to content

fix(websocket): apply WebSocketStream receive backpressure#5562

Open
mcollina wants to merge 2 commits into
mainfrom
fix/websocketstream-backpressure
Open

fix(websocket): apply WebSocketStream receive backpressure#5562
mcollina wants to merge 2 commits into
mainfrom
fix/websocketstream-backpressure

Conversation

@mcollina

@mcollina mcollina commented Jul 15, 2026

Copy link
Copy Markdown
Member

This relates to...

Fixes #5503

Rationale

WebSocketStream continued consuming transport data after its readable queue reached its high-water mark. Slow consumers therefore accumulated an unbounded number of messages instead of propagating backpressure to the peer.

Changes

Features

N/A

Bug Fixes

  • Consume upgraded WebSocket sockets with .read() from the 'readable' event instead of flowing 'data' events.
  • Read for WebSocketStream only while the readable controller has demand and the frame parser is ready.
  • Continue reading during the closing handshake so Close frames can be processed.
  • Mark the existing WebSocketStream receive-backpressure WPT as passing.

Breaking Changes and Deprecations

N/A

Status

Validation

  • npm run lint
  • npm run test:websocket
  • node test/web-platform-tests/wpt-runner.mjs run '/websockets/stream/tentative/backpressure-receive.any.html?wss' '/websockets/stream/tentative/backpressure-send.any.html?wss'

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina
mcollina requested review from KhafraDev and tsctx and removed request for tsctx July 15, 2026 10:25
@codecov-commenter

codecov-commenter commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.48%. Comparing base (6010668) to head (d74a456).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
lib/web/websocket/websocket.js 88.88% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5562      +/-   ##
==========================================
+ Coverage   93.44%   93.48%   +0.04%     
==========================================
  Files         110      110              
  Lines       37434    37541     +107     
==========================================
+ Hits        34979    35094     +115     
+ Misses       2455     2447       -8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.


// 7. Apply backpressure to the WebSocket.
// TODO
// This is done when the socket becomes available.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is wrong and could possibly lead to dropped messages.

  • user does new WebSocketStream(...)
  • await ws.opened
  • sets up the ReadableStream idk, 10 seconds later

any messages between the connection opening and reading messages will get "dropped" (stream will pull them even if the user isn't)

start: (controller) => {
this.#readableStreamController = controller
},
pull: () => this.#resumeSocketIfNeeded(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

" // 5. Let pullAlgorithm be an action that pulls bytes from stream ."

#resumeSocketIfNeeded does not do that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

doesn't it make sense to handle backpressure in the WHATWG stream instead of the node socket? This implementation doesn't look correct to me

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What do you mean? When the WebSocket is pulled, the stream restarts.

@mcollina mcollina Jul 17, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If I understand you correctly, you want to switch this from using on('data') to using .read() and .on('readable'), essentially making this a pull rather than a push?

I didn't do that because you usually prefer to avoid large refactorings. I'll get it done.


// 4. Apply backpressure to the WebSocket.
if (this.#readableStreamController.desiredSize <= 0) {
this.#handler.socket.pause()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ditto wrt node socket vs whatwg stream.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
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.

WebSocketStream: receive backpressure is not implemented — a slow consumer buffers every received message in memory

4 participants