bm mcp startup imports the full local FastAPI app (api/__init__.py), ~60% of MCP import time; concurrent starts exceed client connect timeouts · Issue #1731 · basicmachines-co/basic-memory · GitHub
Skip to content

bm mcp startup imports the full local FastAPI app (api/__init__.py), ~60% of MCP import time; concurrent starts exceed client connect timeouts #1731

Open

Description

@groksrc

Summary

bm mcp spends most of its startup time importing the local FastAPI app and every v2 router, even when the server will never serve the local API (for example, when BASIC_MEMORY_FORCE_CLOUD=true routes every tool call to cloud). When several MCP clients start servers at the same moment, those CPU-bound imports compete for cores, and startup can exceed the client's MCP connect timeout. Claude Code's default is about 30 s, and when it is exceeded the client reports MCP servers failed to connect: basic-memory.

Root cause

src/basic_memory/api/__init__.py imports the app eagerly:

from .app import app

So the import chain at MCP startup is:

basic_memory.mcp.tools
  -> mcp.tools.basic_memory_diagnostics   (from basic_memory.mcp.server import mcp)
    -> basic_memory.mcp.server            (from basic_memory.api.container import ApiContainer, installed_container)
      -> basic_memory.api                 (package __init__: from .app import app)
        -> basic_memory.api.app
          -> basic_memory.api.v2.routers  (knowledge_router, prompt_router -> template_loader -> pybars/pymeta grammar, ...)

api/container.py itself is cheap (about 1.6 ms of its own). The cost comes entirely from the package __init__ pulling in .app.

Measurements

Upstream main @ 872f574 (Apple M5 Pro, Python from uv sync, mcp 2.0.0, fastmcp 4.0.3), python -X importtime -c "import basic_memory.cli.main, basic_memory.mcp.tools, basic_memory.mcp.prompts, basic_memory.mcp.resources":

import time:       161 |    1061892 | basic_memory.mcp.tools
import time:       908 |    1016645 |     basic_memory.mcp.server
import time:         8 |     672159 |       basic_memory.api.container
import time:       254 |     672098 |           basic_memory.api.app
import time:       229 |     631944 |                 basic_memory.api.v2.routers
import time:    181183 |     181272 |                   basic_memory.api.v2.routers.knowledge_router
import time:     12357 |     188264 |                   basic_memory.api.v2.routers.prompt_router
import time:     61091 |     174139 |                         pybars._compiler
import time:    112563 |     113004 |                           pymeta.grammar

About 0.67 s of the 1.06 s for basic_memory.mcp.tools is basic_memory.api.app, roughly 63%. A warm import of the full set took 1.7 s, and a cold one 5.5 s.

On a slower machine (Apple M1, 4 performance + 4 efficiency cores; a 0.22.x checkout with the same chain), I timed bm mcp --project main with BASIC_MEMORY_FORCE_CLOUD=true and BASIC_MEMORY_EXPLICIT_ROUTING=true, from spawn to the initialize / tools/list response, with N servers started at once:

N started together initialize median / worst tools/list worst
1 11.2 s / 11.3 s 11.3 s
2 11.7 s / 12.1 s 12.2 s
3 12.8 s / 15.4 s 15.5 s
5 21.0 s / 24.6 s 24.8 s
  • About 10 s of the 11 s solo start comes before the first log line (MCP server constrained to project). That time is imports. /usr/bin/time showed it as CPU (user about 10 s), not I/O. api.app and its routers account for about 4.9 s.
  • DB init, project reconciliation and the watcher take 0.3–1.4 s at every N. Setting BASIC_MEMORY_INDEX_CHANGES=false did not change the N = 5 result (19.8 / 20.2 s), so SQLite and watcher contention are not the cause.
  • In real bursts, where the MCP client itself is also starting, 3 concurrent sessions took about 35 s and missed a 30 s connect timeout. Raising the client timeout to 90 s works around it.

Proposed fix

  1. Make basic_memory/api/__init__.py stop importing .app eagerly. Use a lazy __getattr__, or have callers import basic_memory.api.app directly, so that basic_memory.api.container no longer pulls in FastAPI and the routers.
  2. Check that the MCP startup path (mcp.server and its lifespan) only imports the API app when the local API is actually needed.
  3. Add a startup-time regression check, for example asserting that basic_memory.api.app is not in sys.modules after import basic_memory.mcp.tools, or a -X importtime budget.

Related

Activity

  1. claude commented on Oct 10, 2026

    @claude
    Contributor

    Claude finished @groksrc's task in 15s —— View job


    Triage

    • Type: bug (performance/startup regression). Labeled bug.
    • Component: MCP server startup (import graph). Not Cloud-specific, even though FORCE_CLOUD makes the cost most visible.
    • Priority: Medium-High. Concurrent MCP client starts can exceed the ~30 s connect timeout, so the server fails to connect.
    • Complexity: Simple to Medium. The root cause is confirmed: src/basic_memory/api/__init__.py does from .app import app eagerly, so importing basic_memory.api.container pulls in FastAPI and every v2 router. The fix is a lazy __getattr__ in api/__init__.py or direct basic_memory.api.app imports in callers. The remaining work is checking the mcp.server lifespan and adding an import regression test, such as basic_memory.api.app not in sys.modules after import basic_memory.mcp.tools.

    Duplicates: The search found none. This is related to #740, #886 and #1713, which you already linked.

    Reproduction: The issue already has -X importtime numbers and a concurrency table, so nothing more is needed.

    This is triage only. I haven't made any code changes.

  2. mvanhorn commented on Oct 11, 2026

    @mvanhorn

    Importing basic_memory.api.container first initializes the basic_memory.api package, which eagerly imported the FastAPI app. This pulled the app and its routers into the MCP startup import chain.

    The package now loads and caches the FastAPI instance when its app export is requested. It also preserves that export when the app submodule is imported first, so callers receive the instance in either import order.

    #1740

  3. phernandez commented on Oct 11, 2026

    @phernandez
    Member

    Keeping this open. The plan is to fix it with Python 3.15 explicit lazy imports (PEP 810) once we support 3.15. #1740 was declined in favor of that, to avoid the module __getattr__ / __class__ machinery.

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions