Skip to content

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

@silverable

Description

@silverable

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions