PrefectHQ/fastmcp · error · RuntimeError

Server failed to start after {max_attempts} attempts

Error message

Server failed to start after {max_attempts} attempts

What it means

run_server_in_process polls the spawned server subprocess with a bounded retry loop (attempts with escalating sleeps up to max_attempts). If the port never becomes reachable after max_attempts, the for/else clause raises RuntimeError. It means the server process never started listening, not that the test code is wrong per se.

Source

Thrown at fastmcp_slim/fastmcp/utilities/tests.py:140

    # Wait for server to be running
    max_attempts = 30
    attempt = 0
    while attempt < max_attempts and proc.is_alive():
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.connect((host, port))
                break
        except ConnectionRefusedError:
            if attempt < 5:
                time.sleep(0.05)
            elif attempt < 15:
                time.sleep(0.1)
            else:
                time.sleep(0.2)
            attempt += 1
    else:
        raise RuntimeError(f"Server failed to start after {max_attempts} attempts")

    yield f"http://{host}:{port}"

    proc.terminate()
    proc.join(timeout=5)
    if proc.is_alive():
        # If it's still alive, then force kill it
        proc.kill()
        proc.join(timeout=2)
        if proc.is_alive():
            raise RuntimeError("Server process failed to terminate even after kill")


async def _wait_for_port(host: str, port: int, timeout: float = 5.0) -> None:
    """Poll until a TCP connection to `host:port` is accepted, or raise on timeout."""
    deadline = time.monotonic() + timeout
    while True:
        try:

View on GitHub (pinned to 1f02114297)

Solutions

  1. Check the subprocess output/logs for the real startup exception and fix it
  2. Choose a free port or bind to port 0 and derive the actual port
  3. Increase max_attempts / retry sleeps in the helper if startup is legitimately slow
  4. Verify the server binds the same host the poller connects to (e.g. 127.0.0.1 vs localhost/IPv6)

Example fix

// before
with run_server_in_process(mcp_server, port=8000) as url: ...

// after
with run_server_in_process(mcp_server, port=0) as url: ...  # let OS pick a free port
Defensive patterns

Strategy: retry

Validate before calling

import socket
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(("127.0.0.1", port))
finally:
    s.close()  # raises if port already in use

Try / catch

try:
    with run_server_in_process(mcp_server) as url:
        yield url
except RuntimeError as e:
    if "failed to start" in str(e):
        pytest.fail(f"server subprocess never listened: {e}; check subprocess logs")
    raise

Prevention

When it happens

Trigger: The spawned uvicorn server crashes at import/startup (bad app, missing dependency), the chosen port is already in use, the process is slow to boot past the retry window, or the server binds a different host/port than expected.

Common situations: CI machines with slow startup exceeding the retry budget; port conflicts with other services; exceptions in the server module under test printed in the subprocess but ignored by the test; firewall/IPv4-vs-IPv6 binding issues.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/7f6e7974fce8ccdd. Report an issue: GitHub.