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
- Check the subprocess output/logs for the real startup exception and fix it
- Choose a free port or bind to port 0 and derive the actual port
- Increase max_attempts / retry sleeps in the helper if startup is legitimately slow
- 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
- Capture and assert on subprocess stderr to see real startup errors
- Bind to port 0 to avoid port conflicts
- Raise max_attempts on slow CI runners
- Verify host binding matches the polled host
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
- Server process failed to terminate even after kill
- Server did not start listening on {host}:{port} within {time
- User server did not start on port {mcp_port}
- The device authorization request expired
- Could not restrict access to CLI state
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/7f6e7974fce8ccdd.
Report an issue: GitHub.