Repository navigation
RangeError: Maximum call stack size exceeded in webStandardStreamableHttp.js:639 when multiple transports close simultaneously #1699
Description
SDK Version
1.27.1
Environment
- Node.js 24.14.0
- Linux (Ubuntu 24.04), bare metal
- Clustered server (4 workers via
node:cluster) - 100+ concurrent MCP sessions daily
- Nginx reverse proxy with consistent hash by
Mcp-Session-Id
Description
When multiple MCP transport sessions close simultaneously (e.g., during a server restart, network partition, or when many clients disconnect at once), a recursive promise rejection cascade in webStandardStreamableHttp.js:639 causes a stack overflow that crashes the Node.js process.
The process does NOT exit — it stays alive but becomes completely unresponsive. All new MCP connections hang indefinitely. The only recovery is a full process restart.
Stack Trace
Exception in PromiseRejectCallback:
file:///node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js:639
}
^
RangeError: Maximum call stack size exceeded
Exception in PromiseRejectCallback:
file:///node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js:639
}
^
RangeError: Maximum call stack size exceeded
(This error repeats multiple times in rapid succession)
Reproduction Steps
- Run an MCP server using
StreamableHTTPServerTransportwith 10+ concurrent active sessions - Trigger multiple transport closures simultaneously (e.g.,
systemctl restart, network disconnect, or programmatic close of many transports) - The
onclosecallbacks trigger in rapid succession - Promise rejection handlers recurse until stack overflow
- Process becomes unresponsive —
curlto any endpoint hangs or times out
Impact
- Severity: High — production server becomes completely unresponsive
- Frequency: 2-3 times per day in production with active MCP clients (Claude Code, Cursor, etc.)
- Recovery: Requires full process restart (manual or via systemd watchdog)
- Data loss: Any in-flight MCP tool calls are lost
Workaround
We added an uncaughtException handler that detects this specific error and forces a clean exit, relying on systemd Restart=on-failure to auto-recover:
process.on('uncaughtException', (err) => {
if (err.message?.includes('Maximum call stack size exceeded')) {
console.error('[CRASH] MCP SDK stack overflow — restarting');
process.exit(1); // systemd restarts in ~1s
}
});Combined with a systemd watchdog (WatchdogSec=600) as a second safety net in case the exception handler doesn't fire.
Suggested Fix
The issue appears to be in the transport cleanup/close logic where promise rejection handlers call each other recursively. A possible fix would be to:
- Batch transport closures instead of processing them synchronously
- Use
queueMicrotaskorsetImmediateto break the call stack between closures - Add a guard to prevent re-entrant close processing
Additional Context
- Server uses Express with
StreamableHTTPServerTransport(not SSE transport) - Each worker process manages its own set of transports
- The crash typically happens when 15-25 transports close within the same event loop tick
- We manage ~50-100 active transports per worker at peak
Activity
claygeo commentedon Mar 27, 2026
Opened a fix for this in #1788. The approach combines the best elements from the prior attempts (#1700 and #1705):
_closingflag) prevents recursiveclose()callscleanup()provides error isolation so one failing stream doesn't block othersonclose()fires only after all cleanup is completeThe prior PRs each addressed one or two of these but not all three. Happy to adjust if the maintainers have a preferred approach.
fix(server): add re-entrancy guard to close() to prevent stack overflow