Repository navigation
Conversation
Every event and heartbeat on a GET /mcp stream was written with raw.write() and its return value ignored, so a client that stopped reading made the server queue every broadcast for it until the socket died: 25 MB for 400 notifications of 64 KB. Live writes now go through writeSSE(), which closes a stream once more than 1 MB is waiting for its client. Events are in the session history, so the client resumes with Last-Event-ID and loses nothing. The replay of that history waits for 'drain' instead, since a backlog larger than the limit is normal on reconnect and must not get the stream closed. Fixes #316 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LmhsFjk8W96X2PvmShmDm9
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The legacy
GET /mcpSSE stream writes every event (live sends, the Last-Event-ID replay, heartbeats) withraw.write()and ignores the return value. For a client that stops reading, every broadcast stays in Node's writable buffer until the socket dies. That is 25 MB for 400 notifications of 64 KB, per stalled stream.Fix
writeSSE(). Whenwrite()reports back-pressure and more than 1 MB (MAX_SSE_BUFFERED_BYTES, the same limit as the 2026-07-28 listen streams on feat: implement MCP 2026-07-28 alongside the handshake revisions #170) is waiting for the client, the stream is destroyed. The events are already in the session history, so the client reconnects withLast-Event-IDand misses nothing. The destroyed stream is removed from the session like any other dead stream, so the reconnect is not refused with 409. Node's own writable buffer is the queue. There is no second queue in front of it, because the history already does that job.drain(orclose) wheneverwrite()returns false, instead of writing the whole backlog at once. A backlog bigger than 1 MB is normal on reconnect. Closing the stream for it would make a client that is reading fine reconnect forever.Notes for review:
getMessagesFromwas awaited, so this is not new. Event ids still let the client resume correctly.sendSSEToStreamsonly changes the write call inside the loop, to keep it clear of fix(sse): allocate event ids atomically in the session store (#317) #325.Tests
test/sse-backpressure.test.ts, real server with raw sockets:Lint, typecheck and the full suite pass locally (770 tests). The new file passed 5 out of 5 repeated runs.
Fixes #316
🤖 Generated with Claude Code
https://claude.ai/code/session_01LmhsFjk8W96X2PvmShmDm9