Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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