[BUG] LSP client never sends textDocument/didClose, leaking language-server resources until OOM
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Claude Code's LSP client opens documents (textDocument/didOpen) as it touches files but never sends textDocument/didClose when it is done with them, and it never evicts idle documents. Any language server that allocates per-open-document resources (commonly one worker process per file) therefore grows without bound for the lifetime of the session.
I hit this with a Lean language server (lake serve, see lean-lsp-plugin): as Claude navigated across files, leaked worker processes climbed to 51 alive at once and a transient ~100 GB resident set with heavy swapping that degraded the whole machine. The count only ever grew — no file was ever released. The defect is in the client's document lifecycle, not in the server; any per-document language server should be affected.
What Should Happen?
A language server can only release a document's resources once the client tells it the document is closed. So Claude Code should bound the documents it holds open, via any of:
- send
textDocument/didClosefor documents it has finished with; or - evict idle open documents after a threshold (close the least-recently-used ones); or
- expose a configurable cap on concurrently open LSP documents.
Error Messages/Logs
No crash or stack trace — the symptom is unbounded resource growth. Representative `ps` output as documents accumulate (a Lean server here, but the shape is server-agnostic):
$ ps -o pid,rss,command -ax | grep 'lean --worker' | grep -cv grep
51
$ ps -o pid,rss,command -ax | grep 'lean --worker' | grep -v grep
60123 2113344 .../bin/lean --worker file:///proj/A.lean
60291 1987776 .../bin/lean --worker file:///proj/B.lean
... (49 more, none ever removed) ...
# one process per file ever opened; total RSS ~100 GB; heavy swapping (100k+ pageouts)
Steps to Reproduce
Any language server that spawns a process per open document reproduces this; Lean just makes each leaked document expensive, so the impact is obvious.
- Install a Lean LSP plugin and launch Claude Code from a large Lean + mathlib project root. For the lean-lsp-plugin:
claude plugin marketplace add jcreinhold/lean-lsp-pluginthenclaude plugin install lean-lsp-plugin@lean-lsp-plugin. - Have Claude navigate across many
.leanfiles — hover / go-to-definition / find-references over dozens of files. - In another terminal, watch the per-document server processes:
watch -n5 'ps -o pid,rss,command -ax | grep "lean --worker" | grep -v grep' - Observe the process count grow monotonically and never shrink, even long after each file is no longer in use, and total RSS climb into the tens of GB. No
textDocument/didCloseis sent for the navigated files.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.158
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
- Why it reached ~100 GB with Lean: each Lean worker can hold ~2 GB resident (more on a cold build, where it elaborates from source instead of loading cached artifacts), so ~50 never-closed documents added up fast. Lighter language servers would show the same unbounded-growth shape at a smaller scale.
- A Lean-specific stopgap exists — the lean-lsp-plugin now wraps the server in a process-reaping supervisor (https://github.com/jcreinhold/lean-lsp-plugin) — but a workaround can't see
didOpen/didCloseand has to guess which documents are idle. The general fix is client-side document lifecycle (senddidClose, or evict idle documents), which would help every language, not just Lean.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗