# In what scenarios must an instance refuse to multiplex tool calls across MCP sessions when the SSE stream is half-open **Date of Record:** 2026 November 12 **Scope:** Applies to HTTP-native MCP transports where a single server instance fans multiple client sessions over one process using Server-Sent Events for server-to-client streaming and POST for client-to-server requests. Does not apply to stdio transports, nor to deployments where each session is pinned to a dedicated process/worker with its own event loop. ## Core Distinction The dangerous confusion is between *session liveness* and *stream liveness*. An MCP session is a logical identity carrying negotiated capabilities and a request/response correlation table. An SSE stream is a byte channel that may be half-closed — the client has stopped reading (TCP receive window exhausted, proxy buffering, or a browser tab throttled) — while the session itself remains valid and the client keeps POSTing new tool calls. Multiplexing decisions that key only on session validity will happily enqueue outbound frames for a stream that no longer drains. Because SSE frames share a single ordered channel per session, one blocked frame blocks every subsequent frame on that session. The invariant that must hold is: **outbound frame admission is gated on the target stream's drain credit, not on session existence.** When a session's stream is half-open, the instance must refuse to admit new multiplexed work for that session rather than buffer unboundedly or silently drop ordering. ## What Was Observed Under a fan-out of ~400 concurrent sessions, a client behind a buffering reverse proxy stopped consuming its SSE stream while continuing to POST `tools/call` requests. The server's per-session outbound queue grew monotonically: each POST produced a JSON-RPC response frame enqueued to the half-open stream. The write loop blocked on the socket, but admission control had no visibility into that block — it only checked `session.is_active`. Within ~90 seconds the session's queue exceeded the flow-control high-water mark and the process RSS climbed by ~1.2 GB. Because the request handler awaited queue admission, the client's POSTs began timing out at the gateway, which the gateway retried, which produced duplicate `tools/call` invocations against a non-idempotent tool. The instance never failed; it degraded, which is worse. ## Condition of Validity Refusal is correct only when all of the following hold: 1. The stream is *half-open*: the session is authenticated and active, but the last N write attempts made zero forward progress within the drain window (e.g., socket send buffer full and no `drain` resolution). 2. The target session's outbound queue is at or above its declared flow-control credit (credit is per-session, never global). 3. The incoming request is *session-scoped* — it carries a session ID and expects a response on that session's stream. If the stream is fully closed (client disconnected), the correct action is session teardown, not refusal. If the request is session-independent (e.g., a stateless `initialize`), it must not be refused on account of another session's blocked stream. Per-session isolation means one blocked stream must never consume credit belonging to another session. ## Decision Boundary - **Admit** when drain credit > 0 for the target session's stream, regardless of global load. - **Refuse with backpressure signal** (HTTP 429 with `Retry-After`, or a JSON-RPC error frame if the stream is still writable) when the target session is half-open and at credit zero. Refusal must be *scoped to that session*; do not shed other sessions. - **Tear down** when the stream has been unwritable past the liveness timeout — a half-open stream that never recovers is a broken session, and holding it open leaks credit forever. - **Never** reorder: a refused request must not be admitted later out of sequence ahead of an already-queued frame on the same session. The subtle boundary: refusal must happen *before* the handler is dispatched for non-idempotent tools, otherwise gateway retries duplicate side effects. Admission control belongs at the transport layer, upstream of tool execution. ## Operational Effect Instances that gate on session existence rather than stream drain credit exhibit head-of-line blocking that propagates through retry amplification into duplicated tool side effects and memory growth that looks like a leak but is actually unbounded per-session queueing. Instances that gate on per-session drain credit convert a global degradation into a localized, recoverable backpressure signal: the affected client sees 429s, retries with backoff, the proxy eventually drains or the session is reaped, and other sessions are untouched. The observable signature of a correct implementation is that a single half-open stream produces bounded queue depth and zero duplicate tool invocations. ```python class HalfOpenStreamRefusal(RuntimeError): """Raised when a multiplexed outbound frame targets a half-open, credit-exhausted SSE stream and must not be admitted.""" pass class SessionStream: def __init__(self, session_id: str, credit: int, drain_window_s: float): self.session_id = session_id self.credit = credit self.drain_window_s = drain_window_s self.queue_depth = 0 self.last_progress_s = 0.0 def is_half_open(self, now_s: float) -> bool: # Active session, but no forward progress within the drain window. return self.queue_depth >= self.credit and \ (now_s - self.last_progress_s) >= self.drain_window_s def admit(self, now_s: float, session_scoped: bool, idempotent: bool) -> None: if not session_scoped: return # session-independent work bypasses this gate if self.is_half_open(now_s): if not idempotent: # Refuse BEFORE dispatch so gateway retries cannot duplicate. raise HalfOpenStreamRefusal( f"session {self.session_id}: stream half-open at credit " f"{self.credit}, queue_depth {self.queue_depth}; " f"refusing non-idempotent multiplexed call" ) raise HalfOpenStreamRefusal( f"session {self.session_id}: stream half-open; " f"backpressure required (429 + Retry-After)" ) self.queue_depth += 1 def dispatch_tool_call(stream: SessionStream, now_s: float, session_scoped: bool = True, idempotent: bool = False) -> None: # Admission control lives at the transport layer, upstream of execution. stream.admit(now_s, session_scoped=session_scoped, idempotent=idempotent) # ... enqueue outbound frame, run tool, write to SSE channel ... ```