Skip to content

http-stream transport cannot scale horizontally because sessions are stored in-process #187

@esgn

Description

@esgn

Problem

The current http-stream transport cannot be safely deployed behind a load balancer with multiple replicas.

HttpStreamTransport stores each StreamableHTTPServerTransport instance in an in-memory object indexed by MCP-Session-Id:

private _transports: {
  [sessionId: string]: StreamableHTTPServerTransport
} = {};

When a client initializes a session, the session is created and stored only in the pod that handled the initialization request.

Subsequent requests include the returned MCP-Session-Id. If one of those requests is routed to another pod, that pod cannot find the session in its local _transports map and returns:

404 Session not found

This effectively requires sticky sessions and also means that sessions are lost whenever a pod restarts or is replaced during a rolling deployment.

Current behaviour

With two replicas behind a round-robin load balancer:

  1. initialize is routed to pod A.

  2. Pod A creates session abc.

  3. The client sends a subsequent request with:

    MCP-Session-Id: abc
  4. The load balancer routes the request to pod B.

  5. Pod B does not have abc in its local _transports object.

  6. Pod B returns 404 Session not found.

Expected behaviour

It should be possible to configure the Streamable HTTP transport in stateless mode when the MCP server does not require server-side session state.

The official TypeScript SDK supports this by omitting the session ID generator:

new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined
});

In stateless mode:

  • no MCP-Session-Id is returned;
  • incoming requests do not require a session ID;
  • requests can be routed to any replica;
  • Kubernetes deployments can scale horizontally without session affinity.

Existing configuration appears unused

HttpStreamTransportConfig already exposes a session configuration:

session?: {
  enabled?: boolean;
  headerName?: string;
  allowClientTermination?: boolean;
  maxConcurrentSessions?: number;
  sessionTimeout?: number;
};

However, session.enabled does not appear to be used by HttpStreamTransport.

The transport currently always creates the SDK transport with:

sessionIdGenerator: () => randomUUID()

It also always rejects non-initialization requests without a session ID:

this.sendError(
  res,
  400,
  -32000,
  'Bad Request: No valid session ID provided'
);

As a result, configuring:

session: {
  enabled: false
}

does not actually disable session management.

Proposed solution

Make session.enabled control whether the transport runs in stateful or stateless mode.

For example:

const sessionsEnabled = this._config.session?.enabled !== false;

transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: sessionsEnabled
    ? () => randomUUID()
    : undefined,
  enableJsonResponse: this._enableJsonResponse,
});

The request handling logic would also need a stateless path that:

  • creates or reuses an appropriate transport without requiring MCP-Session-Id;
  • does not store transports in _transports;
  • does not return 400 when the header is absent;
  • does not return 404 for unknown session IDs when sessions are disabled.

A possible configuration would then be:

transport: {
  type: 'http-stream',
  options: {
    responseMode: 'batch',
    session: {
      enabled: false
    }
  }
}

Alternative

If stateless mode is intentionally unsupported, this limitation should at least be documented explicitly:

  • horizontal scaling requires session affinity;
  • sessions are tied to a single process;
  • pod restarts invalidate active sessions;
  • rolling deployments may interrupt clients.

However, since the underlying MCP TypeScript SDK already supports stateless Streamable HTTP, exposing that capability through mcp-framework seems preferable.

Environment

  • mcp-framework: 0.2.22
  • Transport: http-stream
  • Deployment: multiple Kubernetes replicas behind a load balancer

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions