{"record":{"id":"83bde7a3c8fe4071","repo":"PrefectHQ/fastmcp","slug":"server-process-failed-to-terminate-even-after-kill","errorCode":null,"errorMessage":"Server process failed to terminate even after kill","messagePattern":"Server process failed to terminate even after kill","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/utilities/tests.py","lineNumber":151,"sourceCode":"                time.sleep(0.05)\n            elif attempt < 15:\n                time.sleep(0.1)\n            else:\n                time.sleep(0.2)\n            attempt += 1\n    else:\n        raise RuntimeError(f\"Server failed to start after {max_attempts} attempts\")\n\n    yield f\"http://{host}:{port}\"\n\n    proc.terminate()\n    proc.join(timeout=5)\n    if proc.is_alive():\n        # If it's still alive, then force kill it\n        proc.kill()\n        proc.join(timeout=2)\n        if proc.is_alive():\n            raise RuntimeError(\"Server process failed to terminate even after kill\")\n\n\nasync def _wait_for_port(host: str, port: int, timeout: float = 5.0) -> None:\n    \"\"\"Poll until a TCP connection to `host:port` is accepted, or raise on timeout.\"\"\"\n    deadline = time.monotonic() + timeout\n    while True:\n        try:\n            _, writer = await asyncio.open_connection(host, port)\n        except (ConnectionRefusedError, OSError):\n            if time.monotonic() >= deadline:\n                raise RuntimeError(\n                    f\"Server did not start listening on {host}:{port} \"\n                    f\"within {timeout} seconds\"\n                ) from None\n            await asyncio.sleep(0.001)\n        else:\n            writer.close()\n            with suppress(ConnectionResetError, BrokenPipeError):","sourceCodeStart":133,"sourceCodeEnd":169,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/utilities/tests.py#L133-L169","documentation":"After the test server subprocess fails to terminate gracefully (proc.terminate + join(5s)), the helper escalates to proc.kill() and waits 2 more seconds; if the process is still alive it raises RuntimeError. This guards tests against hanging on unkillable subprocesses.","triggerScenarios":"The server process ignores SIGTERM and SIGKILL-equivalent termination — typically due to blocked/uninterruptible state, child processes keeping resources, or platform quirks (e.g. Windows multiprocessing semantics) preventing clean join.","commonSituations":"Servers spawning their own children that inherit the pipe; processes stuck in uninterruptible disk I/O in CI; multiprocessing issues on macOS spawn method; zombie process accumulation in long test sessions.","solutions":["Ensure the server app has no long-running non-daemon children that block exit","Check the OS process table (ps) for a stuck/uninterruptible process and clean it up","Run tests on a different platform/CI image if the spawn method is the cause","Increase the join timeouts in the helper if termination is merely slow"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    with run_server_in_process(mcp_server) as url:\n        yield url\nexcept RuntimeError as e:\n    if \"failed to terminate\" in str(e):\n        psutil.cleanup_processes()  # or inspect/kill stragglers manually\n    else:\n        raise","preventionTips":["Avoid spawning non-daemon child processes inside the server under test","Use daemon processes/threads in test server apps","Check for zombie processes between test runs","Keep join timeouts generous on loaded CI machines"],"tags":["testing","subprocess","process-termination","multiprocessing"],"backgroundTag":"process-wont-terminate","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}