Skip to content

oxfmt/lsp: tinypool child_process workers accumulate indefinitely (~1500+ processes, 30+ GB RAM) #24147

Description

@s00d

Summary

When running oxfmt --lsp in a long-lived editor session (Cursor / VS Code via the Oxc extension), tinypool child_process workers accumulate without being cleaned up, eventually spawning 1000+ orphaned Node processes and consuming 30+ GB of RAM.

This is distinct from #19994 (high CPU/RAM from sortTailwindcss config) — our .oxfmtrc.json has no tailwind options.

Environment

  • OS: macOS (darwin 25.5.0)
  • oxfmt version: 0.52.0
  • Editor: Cursor (extension host spawns node .../oxfmt/bin/oxfmt --lsp)
  • Node: /Users/<user>/.n/bin/node
  • Package manager: pnpm

Observed behavior

After several hours of normal editing with format-on-save / LSP formatting enabled:

$ ps -axo rss,command | rg "tinypool" | wc -l
1567

$ ps -axo rss,command | rg "tinypool" | awk '{rss+=$1} END {printf "%.1f GB\n", rss/1024/1024}'
31.4 GB

Process tree (representative):

Cursor Helper (Plugin): extension-host (user) GitPulse
  └─ node .../oxfmt/bin/oxfmt --lsp          (PPID of tinypool workers)
       └─ node .../tinypool/.../entry/process.js  × ~1500

Same pattern in a second workspace (nuxt-i18n-next).

Worker command line:

/Users/<user>/.n/bin/node \
  /path/to/project/node_modules/.pnpm/[email protected]/node_modules/tinypool/dist/index.js/../entry/process.js

Workaround: pkill -f "tinypool/dist/index.js" — immediately frees ~30 GB.

Expected behavior

Tinypool worker count should stay bounded (≈ --threads, typically single-digit). Workers should be destroyed when the pool is disposed or when the LSP server shuts down.

Suspected root cause

In apps/oxfmt/src-js/cli/worker-proxy.ts:

export async function initExternalFormatter(numThreads: number): Promise<void> {
  pool = new Tinypool({ ... runtime: "child_process" ... });
}

export async function disposeExternalFormatter(): Promise<void> {
  await pool?.destroy();
  pool = null;
}

disposeExternalFormatter() is only called on CLI exit (apps/oxfmt/src-js/cli.ts), but in --lsp mode the process stays alive indefinitely.

If initExternalFormatter() is called more than once during LSP lifetime (e.g. per format request, config reload, or project switch) without destroying the previous pool first, the old Tinypool instance and its child_process workers would be orphaned — matching the observed accumulation.

Related issues

None of these describe unbounded tinypool worker process accumulation in oxfmt --lsp.

Suggested fix directions

  1. Guard initExternalFormatter: if pool !== null, call disposeExternalFormatter() first (or no-op if thread count unchanged).
  2. Ensure LSP shutdown / workspace folder removal calls disposeExternalFormatter().
  3. Consider --threads=1 or reusing a single pool for the entire LSP session.
  4. Add an integration test: run LSP format N times, assert child process count stays bounded.

Reproduction (approximate)

  1. Open a JS/TS project with oxfmt as formatter (Oxc VS Code/Cursor extension).
  2. Use the project normally for several hours with LSP formatting active.
  3. Monitor: ps aux | rg tinypool | wc -l

Happy to provide more logs or test a patch if needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions