Skip to content

Recommended deployment pattern for FastMCP on AWS Lambda using Mangum #3121

Closed

Description

@shoaib1207k

Question

Hi! I'm deploying a FastMCP server on AWS Lambda using Mangum and wanted to check if I'm following the recommended deployment pattern.

I created the FastMCP instance globally so that tool registration only happens during Lambda cold starts.

# mcp_server.py
mcp = FastMCP(...)

# lambda_handler.py
from mangum import Mangum
from mcp_server import mcp

app = mcp.streamable_http_app()
handler = Mangum(app)

The first invocation succeeds, but subsequent warm invocations fail with:

StreamableHTTPSessionManager.run() can only be called once per instance

Working approach

The only approach I've found that works consistently is recreating the FastMCP instance for every invocation:

def handler(event, context):
    mcp = create_mcp()
    app = mcp.streamable_http_app()
    return Mangum(app)(event, context)

This resolves the issue, but it also means tool registration and server initialization happen on every request instead of only during cold starts.

Is recreating the FastMCP instance per Lambda invocation the recommended deployment pattern?

Or is there a supported way to reuse a global FastMCP instance across warm Lambda invocations while using Mangum?

If this is expected behavior, I'd also appreciate any guidance on the recommended deployment pattern for FastMCP on AWS Lambda.

Thanks!

Additional Context

Environment:-

  • MCP Python SDK: 1.28.1
  • Python: 3.12
  • Mangum: 0.21.0
  • AWS Lambda (Function URL)
  • Transport: streamable-http

Activity

  1. ashiksharonm commented on Jul 18, 2026

    @ashiksharonm

    Root-caused this by checking Mangum's actual source (mangum/adapter.py): Mangum.__call__ creates a new LifespanCycle and enters/exits it on every single invocation, not just cold starts. streamable_http_app() wires the Starlette lifespan to session_manager.run(), which is documented as callable exactly once per instance. So the second invocation against a module-level global app sends a second lifespan.startup, which raises RuntimeError: StreamableHTTPSessionManager.run() can only be called once per instance. This isn't Mangum-version-specific or a misconfiguration — it happens regardless of stateless_http, since that flag doesn't change the lifespan wiring.

    I verified this two ways: reproduced the exact RuntimeError with a global instance across two invocations, and confirmed 3 clean invocations with the app rebuilt fresh inside the handler (your workaround is correct — and cheap, since tool registration is just in-memory bookkeeping, not I/O).

    Happy to open a PR adding an examples/servers/aws-lambda-streamablehttp/ example (factory function + Lambda handler + README explaining the above) if that'd be useful — let me know.

    (Disclosure: used Claude Code to help navigate the SDK/Mangum source and draft this; I've reviewed and verified the repro myself.)

  2. maxisbey commented on Jul 24, 2026

    @maxisbey
    Contributor

    The root-cause above is right: Mangum runs the ASGI lifespan on every invocation, the app's lifespan enters StreamableHTTPSessionManager.run(), and run() is single-use per instance by design (regardless of stateless_http), so a module-level app fails on the first warm invocation — building the app inside the handler is the correct pattern, not a workaround. On your 1.28 line the FastMCP instance caches a single session manager, so a fresh instance per invocation is what you need there; on v2 (where FastMCP is renamed MCPServer) streamable_http_app() builds a new session manager on every call, so you can keep mcp = MCPServer(...) at module scope, with tools registered once at cold start, and only call mcp.streamable_http_app() inside the handler before wrapping it in Mangum. For Lambda you'll likely also want json_response=True (one JSON body per request instead of a held-open SSE stream) and stateless_http=True, since there's no session affinity — the transport options are listed under Running your server → Streamable HTTP. Finally, pass transport_security= with your Function URL's hostname: by default streamable_http_app() arms DNS-rebinding protection with a localhost-only allowlist and answers everything else with 421, which Deploy & scale walks through.

    Closing this as part of a wider backlog cleanup around the v2 release. Feel free to reopen if this is still relevant.

    AI Disclaimer

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

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions