MP-301g · Module 3
WebSocket Fallback
3 min read
MCP's Streamable HTTP transport uses SSE for server-to-client streaming, but some environments handle WebSockets better than SSE. Browser-based MCP clients, mobile applications, and environments behind WebSocket-friendly load balancers may benefit from a WebSocket transport layer. The WebSocket upgrade happens on the same endpoint as the HTTP transport — the client sends an Upgrade: websocket header, and if the server supports it, the connection upgrades to a full-duplex WebSocket. If the server does not support WebSocket, it ignores the upgrade header and responds with standard HTTP, allowing graceful degradation.
WebSocket framing changes the message delivery model. Unlike SSE (which is server-to-client only, with POST for client-to-server), WebSocket is bidirectional on a single connection. Both client and server send JSON-RPC messages as WebSocket text frames. This simplifies the connection model — one connection instead of two — but introduces new concerns: WebSocket connections hold a persistent TCP connection per client, which limits horizontal scaling. Each server instance can handle thousands of WebSocket connections, not millions. Plan capacity accordingly.
Implementing a transport abstraction layer is essential when supporting multiple transports. Define an interface with connect(), send(), onMessage(), and close() methods. Implement it for stdio, Streamable HTTP (SSE + POST), and WebSocket. The MCP client and server logic should be transport-agnostic — they send and receive JSON-RPC messages through the abstraction without knowing whether the underlying transport is a pipe, an HTTP stream, or a WebSocket. This lets you add new transports (QUIC, Unix sockets) without modifying protocol logic.
Do This
- Build a transport abstraction layer so protocol logic is transport-agnostic
- Attempt SSE first, fall back to WebSocket, then fall back to polling
- Monitor WebSocket connection counts per instance for capacity planning
- Implement clean WebSocket close handshakes with status codes (1000, 1001, 1011)
Avoid This
- Hard-code transport logic into the MCP protocol handler — you will regret it when adding a new transport
- Default to WebSocket because it is bidirectional — SSE is simpler and sufficient for most MCP use cases
- Ignore WebSocket connection limits — each connection holds a TCP socket and memory
- Mix HTTP and WebSocket messages on the same logical session without clear transport negotiation