Skip to content

[Bug]: Excessive CPU usage on Windows #4182

Closed

Description

@SlyEcho

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

  1. Start the app on Windows
  2. Open Process Explorer
  3. Observe constant subprocess creation through powershell.exe, cmd.exe, conhost.exe, including:
    • git commands
    • PowerShell script that lists all open TCP connections
    • PowerShell script that lists all running processes
    • taskkill.exe
    • provider update checks (thankfully this can be disabled)

Expected behavior

  • The app runs efficiently and does not waste system resources.
  • It uses native APIs to query system information (WMI or something).
  • When idle, app should not do anything.
  • Does not query system information constantly.

Actual behavior

  • App causes the whole system to slow down and consume CPU leading to fan noise, heat and battery drain on the laptop.
  • App should not use subshells and scripts to query system information.
  • When idle, the app is still busy, even with no active session or user interaction
  • Reads system information in a tight loop without even surfacing this information to the user for unknown reasons.

Impact

Major degradation or frequent failure

Version or commit

0.0.29-nightly.20260720.856

Environment

Windows 11

Logs or stack traces

Screenshots, recordings, or supporting files

Screen.Recording.2026-07-20.143341.mp4

Workaround

No response

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Jul 20, 2026
  2. Armancollab commented on Jul 25, 2026

    @Armancollab

    Having the same issue on windows 10

  3. PLN commented on Jul 27, 2026

    @PLN

    Thanks for filing this. I hit the same thing and went looking through the source to find out where the subprocesses were coming from. Posting what I found in case it helps triage, since I think every item on your list maps to a specific loop.

    Line references are from main as of 2026-07-27; you were on 0.0.29-nightly.20260720.856, so numbers may drift slightly.


    "PowerShell script that lists all running processes": there are two of these

    apps/server/src/diagnostics/ProcessResourceMonitor.ts sets SAMPLE_INTERVAL_MS = 5_000 (line 20) and starts the sampler via Effect.forever(sampleOnce...).pipe(Effect.forkScoped) (line 307). The layer is wired unconditionally in apps/server/src/server.ts:340, so it runs from server start and is not gated on the Diagnostics settings pane being open. That matches your observation that the data is never surfaced.

    Each tick calls readWindowsProcessRows() in diagnostics/ProcessDiagnostics.ts, which spawns powershell.exe -NoProfile -NonInteractive -Command:

    $processes = Get-CimInstance Win32_Process | ForEach-Object {
      $perf = Get-CimInstance Win32_PerfFormattedData_PerfProc_Process `
                -Filter "IDProcess = $($_.ProcessId)" -ErrorAction SilentlyContinue
      ...
    }

    This enumerates every process, then makes one additional CIM round-trip per process. On a ~250-process machine that is ~251 WMI queries every 5 seconds.

    PROCESS_QUERY_TIMEOUT_MS is 1_000. In my testing that query cannot complete in a second, so it times out, is killed, recorded via recordSamplingFailure, and re-run 5 seconds later. The full cost is paid every cycle while producing no usable samples.

    For contrast, the POSIX branch readPosixProcessRows() is a single ps -axo pid=,ppid=,..., which is why this is Windows-specific.

    apps/server/src/terminal/Manager.ts contains the second one. windowsInspectSubprocess() (line 623) runs a full Get-CimInstance Win32_Process enumeration through another powershell.exe, on a DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000 loop (line 79, polled at line 2109) per active terminal session. So with terminals open there is a faster process-listing loop stacked on top of the first.

    "PowerShell script that lists all open TCP connections"

    apps/server/src/preview/PortScanner.ts sets POLL_INTERVAL = Duration.seconds(3) (line 53). The win32 branch (~line 243) spawns powershell.exe running Get-NetTCPConnection -State Listen piped into a per-socket Get-Process -Id. Get-NetTCPConnection is CIM-backed and slow on its own; the per-listener lookup compounds it.

    "taskkill.exe": probably a consequence of the above

    windowsInspectSubprocess sets maxOutputBytes: 32_768 with outputMode: "truncate". A full Win32_Process listing on a busy machine exceeds 32 KB comfortably, so the truncation path in apps/server/src/stream/collectUint8StreamText.ts fires. There is already a comment there acknowledging exactly this:

    keep draining after truncation so the child process can exit normally. its a know issue that on windows killing after the output cap can force an expensive taskkill operation and hurt performance

    The same helper is used by ProcessDiagnostics.runProcess with a 2 MB cap, and that one is killed on the 1s timeout every cycle. So the taskkill.exe you're seeing looks like a downstream effect of the two process-listing loops rather than an independent source, which is good news: fixing the queries should remove it.

    "git commands"

    • apps/server/src/vcs/VcsStatusBroadcaster.ts:27 sets DEFAULT_VCS_STATUS_REFRESH_INTERVAL = Duration.seconds(30)
    • apps/server/src/vcs/GitVcsDriverCore.ts:50 sets STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15)

    Each refresh is several git.exe spawns rather than one (rev-parse --abbrev-ref, rev-parse @{upstream}, status --porcelain=2 --branch, rev-list --left-right --count, for-each-ref, diff --numstat), multiplied by registered projects and worktrees. Reasonable on Linux; on Windows each spawn carries process-creation overhead plus Defender inspection of the binary and the index reads. Secondary to the PowerShell loops, but it adds up.

    "provider update checks"

    For completeness: SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5) in each of provider/Drivers/{Claude,Codex,Cursor,Grok,OpenCode}Driver.ts. As you noted, this one is already disableable, and at 5 minutes it is much less of a concern than the rest.

    I did not chase down the cmd.exe spawns, so I can't add anything there.

    No configuration escape hatch

    Grepping the config surface, the available T3CODE_* variables cover telemetry, tracing, OTLP export, host, home directory, CORS origins, browser launch and Tailscale serve. There is currently no way to disable the resource monitor, the port scanner or the terminal subprocess inspector, or to tune their intervals, short of patching and rebuilding.

    Suggested fixes, roughly by impact

    1. Remove the N+1 in readWindowsProcessRows. Query Win32_Process once and Win32_PerfFormattedData_PerfProc_Process once unfiltered, then join on IDProcess in PowerShell. Two CIM calls instead of ~251, which should clear the existing 1s timeout easily.
    2. Make the background sampler lazy. Only run the Effect.forever loop while a client is actually subscribed to diagnostics. This also removes the (smaller) idle cost on macOS and Linux, and addresses your "reads system information without surfacing it" point directly.
    3. Narrow windowsInspectSubprocess. It only needs the descendants of one terminal PID, so a -Filter on ParentProcessId, or a cached full listing shared across sessions, would avoid both the repeated full enumeration and the 32 KB truncate/taskkill path. A 1s interval also seems aggressive for what it's used for.
    4. Raise or make configurable the PortScanner interval on Windows, and give the listener lookup the same single-query treatment.
    5. Reconsider PROCESS_QUERY_TIMEOUT_MS. If a query reliably times out, re-running it on a fixed interval is pure cost. Backoff after repeated failures, or disabling the sampler after N consecutive timeouts, would bound the damage even if the query itself isn't changed.
    6. Optionally add env toggles (e.g. T3CODE_PROCESS_DIAGNOSTICS_ENABLED, T3CODE_PORT_SCAN_INTERVAL_MS) so people can opt out without forking.
  4. Hexalyse commented on Jul 28, 2026

    @Hexalyse

    I can confirm I experienced the same problem, tried to diagnose it, and came to the same conclusion. Spawning powershell and calling Get-CimInstance creates huge spikes of CPU usage.
    And ironically, the timeout associated kills some of those processes before they can return (hence the taskkill calls). So I guess T3Code isn't even getting the info it's trying to fetch. And it does that every 5 seconds.

    If it's not a necessary thing, and no clean solution can be easily implemented for Windows in the short term, maybe having an option to disable this telemetry could be a good momentary solution ?

  5. bzbetty commented on Jul 28, 2026

    @bzbetty

    yeah seems t3code gets in weird loops where it just spams lots of git.exe processes

    Image
  6. bzbetty commented on Jul 29, 2026

    @bzbetty

    potentially fixed in #4727

  7. PLN commented on Jul 29, 2026

    @PLN

    The git ones are annoying. They can be made worse if you have an issue in your .git trees. Have your agent go through and fsck your .git directories to make sure.

    But in my experience the WMI 'process monitoring' is worse, because it locks the performance-data subsystem, so anything else looking for process or thread state, like Task Manager, or even DWM, all have to wait for these wmi queries. That dosses your whole PC.

    Claude worked out these two patches to 'bin.mjs' to work around the whole issue:

    const string Marker = "T3_TELEMETRY_DISABLED";
    
    // The sampler: return after an idle wait, leaving the original WMI query unreachable.
    const string SamplerAnchor = "function readWindowsProcessRows() {\n\tconst command = [";
    const string SamplerPatched =
    	"function readWindowsProcessRows() {\n" +
    	"\t/* " + Marker + " */ return Effect.sleep(Duration.millis(800)).pipe(Effect.map(() => []));\n" +
    	"\tconst command = [";
    
    // The signal guard: keep the self-pid check, drop the descendant lookup (it reads the
    // same, now-empty, process table and would otherwise fail closed - see the runbook).
    const string GuardAnchor =
    	"\tif (pid === process.pid) return Effect.fail(new ProcessDiagnosticsServerProcessSignalError({ pid }));\n" +
    	"\treturn readProcessRows.pipe(";
    const string GuardPatched =
    	"\tif (pid === process.pid) return Effect.fail(new ProcessDiagnosticsServerProcessSignalError({ pid }));\n" +
    	"\t/* " + Marker + " */ return Effect.void;\n" +
    	"\treturn readProcessRows.pipe(";
    
    
  8. PLN commented on Jul 30, 2026

    @PLN

    #2679 seems to have entirely fixed this issue, replacing the powershell logic with a minimal resource using t3-resource-monitor.exe.
    Thank you!

  9. t3-code commented on Aug 4, 2026

    @t3-code
    Contributor

    Closing as fixed by merged PR #2679, with the fix also confirmed in this issue: #2679

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.needs-triageIssue needs maintainer review and initial categorization.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions