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
- Guard
initExternalFormatter: if pool !== null, call disposeExternalFormatter() first (or no-op if thread count unchanged).
- Ensure LSP shutdown / workspace folder removal calls
disposeExternalFormatter().
- Consider
--threads=1 or reusing a single pool for the entire LSP session.
- Add an integration test: run LSP format N times, assert child process count stays bounded.
Reproduction (approximate)
- Open a JS/TS project with
oxfmt as formatter (Oxc VS Code/Cursor extension).
- Use the project normally for several hours with LSP formatting active.
- Monitor:
ps aux | rg tinypool | wc -l
Happy to provide more logs or test a patch if needed.
Summary
When running
oxfmt --lspin a long-lived editor session (Cursor / VS Code via the Oxc extension), tinypoolchild_processworkers 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
sortTailwindcssconfig) — our.oxfmtrc.jsonhas no tailwind options.Environment
node .../oxfmt/bin/oxfmt --lsp)/Users/<user>/.n/bin/nodeObserved behavior
After several hours of normal editing with format-on-save / LSP formatting enabled:
Process tree (representative):
Same pattern in a second workspace (
nuxt-i18n-next).Worker command line:
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:disposeExternalFormatter()is only called on CLI exit (apps/oxfmt/src-js/cli.ts), but in--lspmode 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 itschild_processworkers would be orphaned — matching the observed accumulation.Related issues
sortTailwindcss(different trigger, closed)--threads(container context)None of these describe unbounded tinypool worker process accumulation in
oxfmt --lsp.Suggested fix directions
initExternalFormatter: ifpool !== null, calldisposeExternalFormatter()first (or no-op if thread count unchanged).disposeExternalFormatter().--threads=1or reusing a single pool for the entire LSP session.Reproduction (approximate)
oxfmtas formatter (Oxc VS Code/Cursor extension).ps aux | rg tinypool | wc -lHappy to provide more logs or test a patch if needed.