Summary
On Windows, a stdio_server()-based MCP server survives a system suspend/resume
cycle as a live process but its event loop starts spinning: one thread burns a
full CPU core indefinitely while the server no longer serves any requests. The
process never exits and never recovers, so it keeps a core pinned until it is
killed manually.
I hit this with two server instances at once, each pinning a core (~2 cores lost
on an 8-core machine) for nearly two hours before I noticed.
Environment
|
|
mcp |
1.29.0 |
anyio |
4.14.2 |
| Python |
3.13.14 (uv-managed CPython, cpython-3.13-windows-x86_64-none) |
| OS |
Windows 10 Pro N, build 19045 |
| Host |
Claude Desktop 1.40609.0.0 (stdio transport) |
| Server |
a FastMCP server using the default mcp.run() stdio path |
| Event loop policy |
WindowsProactorEventLoopPolicy (Python default on Windows) |
What happens
- The host launches the server over stdio. It initializes normally —
initialize, tools/list, prompts/list, resources/list all complete.
- The machine goes to sleep.
- The machine wakes.
- From the moment of resume, the server's main thread spins at 100% of one
core, forever. No further MCP traffic is logged. The process does not exit.
Evidence that resume is the trigger
The host log recorded the resume precisely:
19:02:32 [warn] [event-loop-stall] main process blocked for 8065382ms [likely sleep: power_event]
19:02:32 [info] Timeout detector: tick arrived 8008s late — treating as system wake
19:02:39 [info] [event-loop-stall] OS resume
Accumulated CPU time of the two server processes, sampled at 20:55:11:
| PID |
CPU time |
wall time since resume (19:02:39) |
ratio |
| A |
6719 s |
6752 s |
99.5 % |
| B |
6625 s |
6752 s |
98.1 % |
So both processes have been burning ~100% of a core continuously since the exact
second of resume, and were healthy before it. A 5-second live sample confirmed
100.3% of one core each at the time of measurement.
Thread-level detail
Only the main thread is hot; the worker threads are idle:
PID A
Id ThreadState WaitReason TotalCPU_s
5284 Running 6637.00
13812 Wait UserRequest 0.00
6312 Wait Executive 0.00
This is what makes me think the spin is in the event loop itself rather than in
the anyio.wrap_file(...) stdin reader: if the reader thread were hitting EOF in
a tight loop, the CPU would show up on a worker thread, not the main one.
Hypothesis (not confirmed with a debugger)
WindowsProactorEventLoopPolicy drives the loop off an IOCP. My guess is that
the completion port or one of the registered handles is left in a state where
GetQueuedCompletionStatus returns immediately after resume, so
ProactorEventLoop._poll() returns with no events and a zero timeout forever.
I want to be clear that I have not attached a debugger to confirm this — the
correlation with resume and the main-thread-only CPU profile are what I actually
observed. If a maintainer can suggest what to capture (a py-spy dump on the
spinning process, for instance) I am happy to reproduce and collect it.
Notes
- Both instances were affected, including the one the host was actively
connected to — so this is not only an orphaned-process problem.
- The server had no network connection open to its backend at the time (no
established TCP socket), i.e. it was genuinely idle work-wise.
- The server itself has no background tasks or polling loops of its own; all its
work is on-demand inside tool calls. Nothing in application code is looping.
Possible mitigations
Two things that might be worth considering, in increasing order of ambition:
- Document it. Even a note that Windows stdio servers can spin after
suspend/resume would have saved me a couple of hours of process forensics.
- Detect the dead pipe and exit. If stdin is unreadable/broken after resume,
the server is useless anyway — exiting cleanly would let the host respawn it,
and would turn a pinned core into a transparent reconnect.
As a local workaround I am considering forcing WindowsSelectorEventLoopPolicy
for this process, since this particular server only needs sockets and threads
(no subprocess transports). I have not yet verified whether that actually avoids
the spin.
Summary
On Windows, a
stdio_server()-based MCP server survives a system suspend/resumecycle as a live process but its event loop starts spinning: one thread burns a
full CPU core indefinitely while the server no longer serves any requests. The
process never exits and never recovers, so it keeps a core pinned until it is
killed manually.
I hit this with two server instances at once, each pinning a core (~2 cores lost
on an 8-core machine) for nearly two hours before I noticed.
Environment
mcpanyiocpython-3.13-windows-x86_64-none)mcp.run()stdio pathWindowsProactorEventLoopPolicy(Python default on Windows)What happens
initialize,tools/list,prompts/list,resources/listall complete.core, forever. No further MCP traffic is logged. The process does not exit.
Evidence that resume is the trigger
The host log recorded the resume precisely:
Accumulated CPU time of the two server processes, sampled at 20:55:11:
So both processes have been burning ~100% of a core continuously since the exact
second of resume, and were healthy before it. A 5-second live sample confirmed
100.3% of one core each at the time of measurement.
Thread-level detail
Only the main thread is hot; the worker threads are idle:
This is what makes me think the spin is in the event loop itself rather than in
the
anyio.wrap_file(...)stdin reader: if the reader thread were hitting EOF ina tight loop, the CPU would show up on a worker thread, not the main one.
Hypothesis (not confirmed with a debugger)
WindowsProactorEventLoopPolicydrives the loop off an IOCP. My guess is thatthe completion port or one of the registered handles is left in a state where
GetQueuedCompletionStatusreturns immediately after resume, soProactorEventLoop._poll()returns with no events and a zero timeout forever.I want to be clear that I have not attached a debugger to confirm this — the
correlation with resume and the main-thread-only CPU profile are what I actually
observed. If a maintainer can suggest what to capture (a
py-spy dumpon thespinning process, for instance) I am happy to reproduce and collect it.
Notes
connected to — so this is not only an orphaned-process problem.
established TCP socket), i.e. it was genuinely idle work-wise.
work is on-demand inside tool calls. Nothing in application code is looping.
Possible mitigations
Two things that might be worth considering, in increasing order of ambition:
suspend/resume would have saved me a couple of hours of process forensics.
the server is useless anyway — exiting cleanly would let the host respawn it,
and would turn a pinned core into a transparent reconnect.
As a local workaround I am considering forcing
WindowsSelectorEventLoopPolicyfor this process, since this particular server only needs sockets and threads
(no subprocess transports). I have not yet verified whether that actually avoids
the spin.