Skip to content

AIO mode with stdio backend leaks one backend process per MCP session (never reaped on session close) #44

Description

@GiovanniSlabs

Summary

When running minibridge in AIO mode wrapping a stdio MCP server (minibridge aio -- postgres-mcp), a backend process is spawned per MCP session and is never terminated when that session ends. Backend processes accumulate over time until the container exhausts memory.

Environment

  • Image: acuvity/mcp-server-postgres:latest (minibridge AIO + crystaldba postgres-mcp 1.25.0)
  • Transport: HTTP (mode=http), client connecting to /mcp (Streamable HTTP) and /sse (legacy) — both affected
  • server-tls=false (TLS terminated by an upstream reverse proxy)
  • Client: OpenAI Responses API, hosted remote MCP tool, require_approval: "never"
  • Deployed via Docker (Swarm), single replica

Steps to reproduce

  1. Start the container (AIO, stdio backend).
  2. Issue several requests from the MCP client over time.
  3. Watch the backend process count inside the container:
    watch -n 2 'docker top <container_id> | grep -c postgres-mcp'
    

Observed behavior

The backend process is spawned per MCP session, not per tool call: within a single session, multiple tools/call correctly reuse one backend. However, the backend is never reaped when the session ends — so one orphaned process leaks per session.

The leak rate depends on the transport:

  • With Streamable HTTP (/mcp): ~1 leaked process per user request (the session is reused across the tool calls within that request).
  • With legacy SSE (/sse): ~1 leaked process per tool call, since each call appears to open a fresh session.

In both cases the orphaned backend (/app/.venv/bin/python3 /app/.venv/bin/postgres-mcp) stays alive as a child of the minibridge aio parent and is never terminated. Each process holds ~95 MB RSS.

Process list after a sequence of requests (parent PID 2005; note the steadily increasing count and the decreasing ELAPSED, one new process per session):

PID    PPID   ELAPSED  RSS     COMMAND
2005   1862   39:08    32136   minibridge aio -- postgres-mcp
11391  1862   19:18    1168    sh
12392  2005   15:48    94876   /app/.venv/bin/python3 /app/.venv/bin/postgres-mcp
12402  2005   15:42    95072   /app/.venv/bin/python3 /app/.venv/bin/postgres-mcp
12415  2005   15:37    95152   /app/.venv/bin/python3 /app/.venv/bin/postgres-mcp
12510  2005   15:32    95124   /app/.venv/bin/python3 /app/.venv/bin/postgres-mcp
12547  2005   15:22    95308   /app/.venv/bin/python3 /app/.venv/bin/postgres-mcp
14909  2005   06:26    94712   /app/.venv/bin/python3 /app/.venv/bin/postgres-mcp
16374  2005   00:36    94984   /app/.venv/bin/python3 /app/.venv/bin/postgres-mcp
16382  2005   00:33    95180   /app/.venv/bin/python3 /app/.venv/bin/postgres-mcp

Over a longer-running session this reached ~182 processes / ~4 GB RSS and eventually exhausted host memory, affecting other containers on the same host.

Expected behavior

When an MCP session ends — or after a period of inactivity — the backend process spawned for it should be terminated (SIGTERM, escalating to SIGKILL after a grace period). Ideally a single long-lived backend would be reused across sessions rather than spawning one per session.

Crucially, reaping should not depend on the client cleanly closing the session, because some clients don't (see below). An inactivity timeout on idle backends would make the fix robust regardless of client behavior.

Client session behavior (context, not the root cause)

The OpenAI Responses API hosted MCP client has documented, inconsistent session-handling behavior that interacts badly with one-backend-per-session: it has been widely reported to open a fresh MCP session per tool call without reusing the Mcp-Session-Id header, and in other cases to send premature DELETE/session-termination requests. This behavior has reportedly appeared, been fixed, and regressed over time.

This explains the transport-dependent leak rate observed above (fresh session per call on SSE vs. per-request reuse on Streamable HTTP). However, the root issue here is on the minibridge side: backends spawned for a session are not reaped when that session goes away, regardless of why it goes away. The bug manifests under any client that does not keep a single long-lived session.

Question

Does minibridge (or the AIO wrapper) support a stateless mode, where requests are handled without binding a long-lived backend process to a session? Servers facing the same OpenAI client behavior commonly work around it by going stateless (e.g. FastMCP's stateless_http=True). If minibridge has an equivalent option, that may both avoid the leak and improve compatibility with the OpenAI client — please point to the relevant config.

Notes

  • tools/list alone does not spawn a backend; sessions that perform tools/call do.
  • Happy to provide additional logs (including the JSON-RPC initialize/DELETE traffic observed for incoming requests) or test a fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions