Skip to content

Every idle MCP client burns ~0.7 core since 0.9.1-rc.1 (0.9.0: 0%) — N concurrent sessions cost N cores, Windows #1764

Description

@PabloSoage

Version

codebase-memory-mcp 0.10.8

Platform

Windows (x64)

Install channel

GitHub release archive / install.sh / install.ps1

Binary variant

standard

What happened, and what did you expect?

A codebase-memory-mcp stdio client process consumes 55-86% of one CPU core while completely idle, from the moment it starts, and never settles. Expected: ~0%, which is exactly what 0.9.0 does on the same machine.

This is not agent-specific and needs no MCP traffic to reproduce: spawning the binary from a plain shell and never sending a single JSON-RPC message already burns a core. The daemon itself is idle (1.2% of one core); the whole cost is in the client processes.

Because each editor/agent session spawns its own client, the cost multiplies. Snapshot from this machine with 5 client processes alive (one per open Claude Code session) plus the shared daemon:

PID     % of one core    RSS
4024        69.5         12.9 MB
37336       67.7         12.2 MB
24208       65.3         13.7 MB
21848       63.9         13.6 MB
10784       47.3         10.4 MB
20812        1.2          6.2 MB   <- the daemon, idle as expected

sum: 3.15 cores = 26% of a 12-thread CPU, permanently, doing nothing

RAM is fine (~13 MB per client). This is purely CPU.

Reproduction

No agent, no editor, no handshake required.

  1. On Windows, spawn the server with stdin held open as a pipe and write nothing to it:
// node spin.js
const { spawn, execSync } = require('child_process');
const p = spawn("C:/path/to/codebase-memory-mcp.exe", [], { stdio: ['pipe','pipe','pipe'] });
p.stdout.on('data',()=>{}); p.stderr.on('data',()=>{});
const cpu = () => parseFloat(execSync(
  'powershell -NoProfile -Command "(Get-Process -Id ' + p.pid + ').TotalProcessorTime.TotalMilliseconds"'
).toString().trim());
setTimeout(() => { const a = cpu(), t = Date.now();
  setTimeout(() => { const b = cpu();
    console.log(((b-a)/(Date.now()-t)*100).toFixed(1) + '% of one core'); p.kill(); }, 10000);
}, 3000);
  1. Run it against 0.9.0 and against 0.10.8.

Metric: delta of TotalProcessorTime over a 10 s wall-clock window, expressed as a percentage of a single core. Measurement starts 3 s after spawn so startup is excluded.

A/B, same machine, alternated runs

Real user profile, 9 indexed projects:

Version run 1 run 2 run 3
0.9.0 0.0% 0.0% 0.1%
0.10.8 70.2% 76.4% 85.9%

Version bisect

Each version run with an isolated LOCALAPPDATA / APPDATA / USERPROFILE / HOME, so every binary got a private daemon endpoint and an empty state directory with zero indexed projects:

Version % of one core
0.9.0 0.0 / 0.0 / 0.1
0.9.1-rc.1 27.0 / 29.5 / 39.1
0.10.0 26.8
0.10.1 17.3
0.10.2 20.0
0.10.4 34.6
0.10.6 30.4
0.10.8 22.7

The regression boundary is exactly v0.9.0 → v0.9.1-rc.1. Absolute numbers are lower in this table than in the A/B above because these runs had no indexed projects; the spin happens regardless.

Logs

Nothing. The loop is completely silent. Measured simultaneously over one 12 s window with CBM_LOG_LEVEL=trace set: 66.8% of one core, and 0 bytes on stderr (0 on stdout too).

Diagnostics trajectory (memory / performance / leak issues)

Thread-level breakdown

One client, alive 24 minutes, still spinning. Two threads carry all of it:

thread 14032    30.5%   Running
thread 22428    29.0%   Wait / ExecutionDelay
thread 14616     0.0%   Wait / Executive

A freshly spawned client shows the same shape: 6 threads, two of them at 50.3% and 54.1%.

Ruled out (each one measured, not assumed)

  • Number of concurrent sessions — a single client, alone, spawned from a plain shell, spins.
  • MCP traffic — spins before any initialize; 62.1% with no handshake at all vs 55.0% after one.
  • The daemondaemon status reports all clients committed; the daemon uses 1.2% of a core.
  • auto_watch — no change with it set to false (47-64% either way). Note the watcher thread is created unconditionally in src/main.c (if (g_watcher)), so the config key does not gate it.
  • auto_indexfalse throughout (this is what distinguishes it from CPU memory usage is too high #1084).
  • Indexed projects — the isolated-home runs above have zero projects and still spin.
  • ui_enabled — 64.4% with --ui=false.
  • --tool-profile=scout — 70.0%.
  • Warm-up — the thread breakdown above was taken on a client that had been alive for 24 minutes; it was still carrying 59.5% of a core across its two hot threads.
  • The parent-death watchdog — excluded on Windows (#ifndef _WIN32) and polls at 500 ms.
  • The watcher's own loopPOLL_BASE_MS 5000 chunked by SLEEP_CHUNK_MS 500 is 10 wakeups per cycle; far too cheap to account for this.

Where I would look (hypothesis, NOT verified)

I could not attribute the CPU to a specific line — the shipped binary is stripped and the loop logs nothing — so please treat this section as a starting point rather than a diagnosis.

The regression window v0.9.0..v0.9.1-rc.1 is 486 commits and contains 0e00ef57 ("feat: coordinate concurrent CBM sessions" — same title as the closed issue #1139, though the commit message itself carries no issue reference), which introduces all of src/daemon/, including src/daemon/frontend.c. That file is the client side: frontend.h describes it as "Stateless stdio bridge for a daemon-backed MCP session", and src/main.c:2946 calls cbm_daemon_frontend_mcp_run(..., stdin, stdout) in the client process. So in this window the client's stdio path went from having no daemon bridge at all to having one built on poll loops:

  • FRONTEND_WAIT_US = 1000 (1 ms) — 3 call sites, including the enqueue path and the join watchdog
  • FRONTEND_IDLE_WAIT_US = 10 * 1000 (10 ms) — the idle path of frontend_worker
  • FRONTEND_MAINTENANCE_POLL_MS = 10frontend_worker calls frontend_exit_for_maintenance() 100 times per second, which calls cbm_version_cohort_maintenance_presence_terminal()

FRONTEND_IDLE_WAIT_US did not exist in 0e00ef57; it was added later, which may explain why the figure varies across 0.10.x instead of being flat.

Secondary observation, possibly unrelated

On Windows cbm_usleep is integer division:

#define cbm_usleep(us) Sleep((DWORD)((us) / 1000))   // src/foundation/compat.h:33

so any argument below 1000 µs becomes Sleep(0), which does not sleep. All the named poll constants are >= 1000 µs, but version_cohort_retry_sleep() (src/daemon/version_cohort.c:93) passes VERSION_COHORT_RETRY_US / 2U + (mix % VERSION_COHORT_RETRY_US) = 500-1499 µs, so roughly half of its jitter range degrades to Sleep(0) on Windows. I do not think this is the driver here (a single isolated client has no lock contention and still spins), but it looked worth flagging.

Not a duplicate of

I also searched issues and PRs for frontend.c, FRONTEND_WAIT_US, FRONTEND_IDLE_WAIT_US, "stdio bridge", "idle cpu" and version_cohort and found nothing covering this. No commit on main after v0.10.8 touches frontend.c, version_cohort.c or main.c, and the three constants are unchanged on main, so this should still be live on tip.

Environment

  • Windows 11 Business 10.0.26200 (x64)
  • 12th Gen Intel Core i7-1255U — 10 physical / 12 logical cores
  • 23.7 GB RAM
  • The installed 0.10.8 binary is byte-identical to the codebase-memory-mcp-windows-amd64.zip release artifact — SHA256 b4b403b1d7c4def3785f148b93f345ce8427858f4f5489ce28580c4387a336a6, matching the build id the daemon reports. Every version in the bisect came from that same windows-amd64 release family, so no build-variant difference is involved. (Note that in 0.10.8 this "standard" artifact does serve the graph UI on 9749.)
  • The 0.9.0 binary came from the installer's own backup, %LOCALAPPDATA%\Programs\codebase-memory-mcp\codebase-memory-mcp.exe.old

Project scale (if relevant)

9 indexed projects on the affected machine. Also reproduces with 0 indexed projects (see bisect table), so scale is not a factor.

Confirmations

  • I searched existing issues and this is not a duplicate.
  • My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingeditor/integrationEditor compatibility and CLI integrationstability/performanceServer crashes, OOM, hangs, high CPU/memoryux/behaviorDisplay bugs, docs, adoption UXwindowsWindows-specific issues

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions