ElixirLS: LSP client spawns new process per request, causing timeout and process leak #8773

Open
opened 2026-02-16 18:10:48 -05:00 by yindo · 1 comment
Owner

Originally created by @silverable on GitHub (Feb 7, 2026).

Originally assigned to: @thdxr on GitHub.

Summary

The builtin ElixirLS LSP integration spawns a new ElixirLS process for every LSP request (symbols, goto_definition, find_references, etc.) instead of maintaining a persistent connection. Each new process must complete its full initialization cycle (installer, mix compile, dialyzer indexing), which takes far longer than the 60s DEFAULT_REQUEST_TIMEOUT_MSEC, so every request times out. Previous processes are never cleaned up, leading to orphaned beam.smp processes accumulating.

Environment

  • OS: macOS (Apple Silicon)
  • opencode: latest (installed via ~/.opencode/bin/opencode)
  • Elixir: 1.19.5
  • OTP: 28
  • ElixirLS: 0.30.0 (installed via Homebrew)

Steps to Reproduce

  1. Open an Elixir project (with mix.exs) in opencode
  2. Use any LSP tool on a .ex file: lsp_symbols, lsp_goto_definition, lsp_find_references
  3. Observe: request times out after 60 seconds
  4. Run ps aux | grep beam.smp — multiple orphaned ElixirLS processes exist

Evidence

After making 2 LSP requests:

$ ps -ax -o pid,lstart,command | grep "launch.exs" | grep -v grep | grep -v phx.server
29215 Sat Feb  7 19:16:17 2026  .../beam.smp ... -extra .../elixir-ls/0.30.0/libexec/launch.exs
30594 Sat Feb  7 19:19:59 2026  .../beam.smp ... -extra .../elixir-ls/0.30.0/libexec/launch.exs

Each request spawned a new process (~3 minutes apart). Neither was cleaned up.

Manual Verification

ElixirLS itself works correctly. Sending a proper LSP initialize request via stdin/stdout directly:

printf 'Content-Length: 150\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file:///path/to/project"}}' | elixir-ls

Returns a full initialize response with all capabilities within seconds.

Root Cause Analysis

Issue 1: No persistent LSP connection

The LSP client appears to spawn a fresh ElixirLS for each tool call rather than maintaining a long-lived connection per workspace root. ElixirLS requires significant startup time (installer check → mix compile → dialyzer indexing), so a per-request spawn model will always timeout.

Issue 2: Server→Client request deadlock (secondary)

ElixirLS sends server→client JSON-RPC requests during initialization (workspace/configuration, window/workDoneProgress/create, client/registerCapability). If the LSP client doesn't respond to these, ElixirLS blocks waiting for replies, preventing it from processing any subsequent client→server requests. This causes a deadlock where the server is alive but unresponsive.

Issue 3: No process cleanup on timeout

When a request times out, the spawned ElixirLS process (beam.smp, ~380MB each) is not killed, leading to resource leaks.

Expected Behavior

  1. A single persistent ElixirLS connection per workspace root, reused across all LSP requests
  2. The LSP client should respond to server→client requests (at minimum: workspace/configuration, window/workDoneProgress/create, client/registerCapability)
  3. On connection failure/timeout, the process tree should be cleaned up

Relevant Code

The builtin config (from strings on the binary):

LSPServer.ElixirLS = {
  id: "elixir-ls",
  extensions: [".ex", ".exs"],
  root: NearestRoot(["mix.exs", "mix.lock"]),
  async spawn(root) {
    let binary2 = Bun.which("elixir-ls");
    return { process: spawn(binary2, { cwd: root }) };
  }
};

Note: ElixirLS's launch.sh uses exec to relaunch in the preferred shell (zsh), then exec again into the BEAM process. This may interact poorly with Bun.spawn's process tracking if it relies on the original PID.

Originally created by @silverable on GitHub (Feb 7, 2026). Originally assigned to: @thdxr on GitHub. ## Summary The builtin ElixirLS LSP integration spawns a **new ElixirLS process for every LSP request** (symbols, goto_definition, find_references, etc.) instead of maintaining a persistent connection. Each new process must complete its full initialization cycle (installer, mix compile, dialyzer indexing), which takes far longer than the 60s `DEFAULT_REQUEST_TIMEOUT_MSEC`, so **every request times out**. Previous processes are never cleaned up, leading to **orphaned `beam.smp` processes accumulating**. ## Environment - **OS**: macOS (Apple Silicon) - **opencode**: latest (installed via `~/.opencode/bin/opencode`) - **Elixir**: 1.19.5 - **OTP**: 28 - **ElixirLS**: 0.30.0 (installed via Homebrew) ## Steps to Reproduce 1. Open an Elixir project (with `mix.exs`) in opencode 2. Use any LSP tool on a `.ex` file: `lsp_symbols`, `lsp_goto_definition`, `lsp_find_references` 3. Observe: request times out after 60 seconds 4. Run `ps aux | grep beam.smp` — multiple orphaned ElixirLS processes exist ## Evidence After making 2 LSP requests: ``` $ ps -ax -o pid,lstart,command | grep "launch.exs" | grep -v grep | grep -v phx.server 29215 Sat Feb 7 19:16:17 2026 .../beam.smp ... -extra .../elixir-ls/0.30.0/libexec/launch.exs 30594 Sat Feb 7 19:19:59 2026 .../beam.smp ... -extra .../elixir-ls/0.30.0/libexec/launch.exs ``` Each request spawned a new process (~3 minutes apart). Neither was cleaned up. ## Manual Verification ElixirLS itself works correctly. Sending a proper LSP `initialize` request via stdin/stdout directly: ```bash printf 'Content-Length: 150\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file:///path/to/project"}}' | elixir-ls ``` Returns a full `initialize` response with all capabilities within seconds. ## Root Cause Analysis ### Issue 1: No persistent LSP connection The LSP client appears to spawn a fresh ElixirLS for each tool call rather than maintaining a long-lived connection per workspace root. ElixirLS requires significant startup time (installer check → mix compile → dialyzer indexing), so a per-request spawn model will always timeout. ### Issue 2: Server→Client request deadlock (secondary) ElixirLS sends server→client JSON-RPC requests during initialization (`workspace/configuration`, `window/workDoneProgress/create`, `client/registerCapability`). If the LSP client doesn't respond to these, ElixirLS blocks waiting for replies, preventing it from processing any subsequent client→server requests. This causes a deadlock where the server is alive but unresponsive. ### Issue 3: No process cleanup on timeout When a request times out, the spawned ElixirLS process (`beam.smp`, ~380MB each) is not killed, leading to resource leaks. ## Expected Behavior 1. A **single persistent ElixirLS connection** per workspace root, reused across all LSP requests 2. The LSP client should **respond to server→client requests** (at minimum: `workspace/configuration`, `window/workDoneProgress/create`, `client/registerCapability`) 3. On connection failure/timeout, the process tree should be **cleaned up** ## Relevant Code The builtin config (from `strings` on the binary): ```javascript LSPServer.ElixirLS = { id: "elixir-ls", extensions: [".ex", ".exs"], root: NearestRoot(["mix.exs", "mix.lock"]), async spawn(root) { let binary2 = Bun.which("elixir-ls"); return { process: spawn(binary2, { cwd: root }) }; } }; ``` Note: ElixirLS's `launch.sh` uses `exec` to relaunch in the preferred shell (zsh), then `exec` again into the BEAM process. This may interact poorly with `Bun.spawn`'s process tracking if it relies on the original PID.
Author
Owner

@github-actions[bot] commented on GitHub (Feb 7, 2026):

This issue might be a duplicate of existing issues. Please check:

  • #11368: JDTLS spawns redundant per-module processes in Java monorepos, causing OOM - This describes the same fundamental architectural problem where LSP spawns new processes instead of maintaining persistent connections. The root cause analysis and suggested fixes from #11368 may directly apply to ElixirLS.

Feel free to ignore if this doesn't address your specific case.

@github-actions[bot] commented on GitHub (Feb 7, 2026): This issue might be a duplicate of existing issues. Please check: - #11368: JDTLS spawns redundant per-module processes in Java monorepos, causing OOM - This describes the same fundamental architectural problem where LSP spawns new processes instead of maintaining persistent connections. The root cause analysis and suggested fixes from #11368 may directly apply to ElixirLS. Feel free to ignore if this doesn't address your specific case.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#8773