A laptop open on a desk showing source code in a dark editor theme

Rust LSP Low Memory: Swap Language Servers

October 25, 2026 · 10 min read · By Thomas A. Anderson

On August 21, 2026, matklad published Rust Glancer, an alternative LSP for Rust that focuses on low memory use. It is a language server, not an editor plugin, so anything that speaks the Language Server Protocol can drive it. That design choice is the practical part: you can point one workspace at it, leave another workspace on rust-analyzer, and compare the two on the same machine without changing editors.

Memory is the usual reason developers go looking for a second server. In most Rust setups the language server is the biggest single process attached to the editor, and on an 8 GB laptop it is the process that decides whether you keep working or start closing browser tabs. The Lobsters discussion that followed the announcement frames the project the same way the announcement does: a lighter alternative to the server most Rust editors ship with.

Key Takeaways:

  • Rust Glancer is an LSP implementation published on 2026-08-21 with low memory use as its stated focus, which makes it swappable per workspace rather than per editor install.
  • VmRSS overstates a server’s true cost when shared libraries are mapped by several processes; PSS splits those pages proportionally and is the number to compare between servers.
  • Before trusting any alternative server, probe its capabilities with an LSP handshake. Missing renameProvider or hoverProvider means your editor silently degrades instead of erroring.
  • Peak RSS and wall time from a wrapper process give you restart and re-index costs that a single ps snapshot will never show.
  • Three options exist, not two: stay on the mainstream server with a scoped workspace, switch to the lean one, or drop the server and lean on cargo check.

What Rust Glancer Changes in the Editor Stack

Every LSP server sits behind the same contract. The editor spawns a child process, writes JSON-RPC messages to its stdin with a Content-Length header, and reads responses from its stdout. The server owns the expensive state: the crate graph, macro expansion results, and inferred types for whatever you have open. Rust makes that state large because a single source file pulls in trait resolutions and generic instantiations from crates you never opened.

Verify a Server Speaks LSP Before Wiring It In

Swapping the server changes two things and leaves everything else alone. The protocol stays identical, so diagnostics, go-to-definition, and hover keep working through the same client code. What changes is how much memory the child process holds and which capabilities it advertises. The first is measurable in a shell; the second is measurable with twenty lines of Python.

Start by recording a baseline on the server you already run. This sampler reads two kernel counters on a fixed interval and prints them in MiB:

Note: The following code is an illustrative example and has not been verified against official documentation. Please refer to the official docs for production-ready code.

#!/usr/bin/env bash
# Sample resident memory of any running language server.
# Usage: ./sample-rss.sh <pid> [samples] [interval_seconds]
set -euo pipefail
pid="${1:?usage: sample-rss.sh <pid> [samples] [interval]}"
samples="${2:-30}"
interval="${3:-2}"

for _ in $(seq 1 "$samples"); do
 ts="$(date +%H:%M:%S)"
 rss_kb="$(awk '/^VmRSS:/ {print $2}' "/proc/$pid/status")"
 pss_kb="$(awk '/^Pss:/ {print $2}' "/proc/$pid/smaps_rollup")"
 printf '%s rss=%dMiB pss=%dMiB\n' "$ts" $((rss_kb / 1024)) $((pss_kb / 1024))
 sleep "$interval"
done

# Output shape (your values will differ with workspace size):
# 09:14:02 rss=1187MiB pss=903MiB
# 09:14:04 rss=1191MiB pss=906MiB
# Note: smaps_rollup requires read permission on the target process; run as
# the same user that launched the server.

Find the pid with pgrep -f rust-analyzer or whatever binary name your client spawns. The binary name for the alternative server is in its own documentation, and the announcement post is the authoritative place to check install steps and flags rather than guessing from the project name.

Measure the Server You Already Run

Run the sampler through a realistic sequence instead of leaving it on an idle editor. Open a workspace, wait for indexing to finish, edit a file in the largest crate, then request a rename on a symbol used across modules. Rename and find-references are the operations that force the server to hold the most state, so they expose the peak you actually care about.

The two counters answer different questions. VmRSS counts every resident page mapped into the process and charges shared pages to each process that maps them, so two Rust servers sharing the same allocator libraries each report the full cost of those pages. PSS splits shared pages proportionally, which makes it the fairer number when you compare a server against its replacement on the same machine. The kernel documentation for proc(5) spells out both fields.

Memory counters you can read without a profiler
Metric Where to read it What it counts Blind spot
VmRSS /proc/<pid>/status Resident pages mapped into the process, with shared pages counted in full Overstates unique cost when several processes map the same libraries
Pss /proc/<pid>/smaps_rollup Resident pages with shared pages divided proportionally across mappers Needs permission to read the target process’s smaps_rollup
Peak RSS getrusage(2), or /usr/bin/time -v output line “Maximum resident set size” High-water mark across the process lifetime One number with no timestamp, so you cannot tell when the peak happened

Record the numbers in a note with the commit hash and the workspace name. A server that uses less memory on a three-crate service can lose on a workspace with thirty crates and heavy macro use, and you will not know which case you hit without a written baseline.

Verify a Server Speaks LSP Before Wiring It In

Capability negotiation is where alternative servers quietly differ. The client sends its own capabilities during initialize, the server replies with a capability map, and the editor enables features based on that map. If renameProvider is absent, the rename command simply does nothing. No error, no warning, just a keystroke that fails to open a prompt.

This probe starts any server binary, performs the handshake, and prints the three capabilities that matter most for day-to-day Rust work:

Note: The following code is an illustrative example and has not been verified against official documentation. Please refer to the official docs for production-ready code.

#!/usr/bin/env python3
"""Minimal LSP probe: initialize a server, print key capabilities, shut it down."""
import json
import subprocess
import sys

def send(proc, payload):
 body = json.dumps(payload).encode()
 proc.stdin.write(b"Content-Length: %d\r\n\r\n" % len(body) + body)
 proc.stdin.flush()

def read_message(proc):
 headers = {}
 while True:
 line = proc.stdout.readline()
 if line in (b"\r\n", b"\n", b""):
 break
 key, _, value = line.decode().partition(":")
 headers[key.strip().lower()] = value.strip()
 return json.loads(proc.stdout.read(int(headers["content-length"])))

# Usage: python3 lsp_probe.py /path/to/server-binary (add flags after the path)
proc = subprocess.Popen(sys.argv[1:], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

send(proc, {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
 "processId": None,
 "rootUri": "file:///home/dev/projects/billing-service",
 "capabilities": {},
}})
caps = read_message(proc)["result"]["capabilities"]
send(proc, {"jsonrpc": "2.0", "method": "initialized", "params": {}})

for name in ("hoverProvider", "definitionProvider", "renameProvider"):
 print(f"{name}: {caps.get(name, False)}")

send(proc, {"jsonrpc": "2.0", "id": 2, "method": "shutdown", "params": None})
read_message(proc)
send(proc, {"jsonrpc": "2.0", "method": "exit"})
print("exit code:", proc.wait(timeout=10))

# Example output from a server with navigation but no rename:
# hoverProvider: True
# definitionProvider: True
# renameProvider: False
# exit code: 0
# Note: this probe ignores server-initiated requests, workspace/configuration
# replies, and timeouts. A real client must handle all three or the server
# will block waiting for answers it never gets.

The last comment is the pitfall that wastes an afternoon. Servers send requests back to the client during startup, most often for configuration and for file watcher registration. A probe that writes the initialize response to stdout but never replies to those requests will hang on a real workspace even though it worked on an empty directory.

Trade-Offs When You Leave the Mainstream Server

A low-memory server buys headroom on the machine and spends some of it back in features. The capability map is the honest inventory: whatever the server does not advertise is a feature your editor will not offer on that workspace. Check the announcement post for the current scope before you plan a switch, since a young server’s feature list changes faster than any write-up about it.

There are three workable configurations, and they suit different machines.

  • Stay on rust-analyzer with a narrower workspace. Fewer crates in the graph means less state to hold. This costs nothing in features and works best when you can split a monorepo into smaller workspace roots. The trade-off is project layout surgery and losing cross-crate jumps at the boundary.
  • Switch to the lean server per workspace. You get the memory reduction on the machine that needs it, and you keep a second workspace on the full server for the days you need its refactors. The trade-off is two installs to update and an editor config that branches on directory.
  • Run no server at all. A terminal running cargo check --workspace on save plus grep for navigation uses almost nothing, but you lose diagnostics as you type and every jump becomes a search. This fits CI-style editing sessions and small single-crate projects.

Do not judge an alternative server by its idle footprint alone. The cost that hurts on a 4-core laptop is the re-index after a restart, when the server rebuilds state and holds the peak instead of the steady-state figure. That is why the next measurement wraps the process rather than sampling it.

Running a Lean Server on Constrained Hardware

An 8 GB laptop with a browser, a container runtime, and an editor competes for the same pages. A separate write-up walks through running the server locally on 8 GB of RAM, and the setup steps there are worth reading before you commit to a config. The measurement below complements it by capturing the peak and the wall time of one server lifetime:

Note: The following code is an illustrative example and has not been verified against official documentation. Please refer to the official docs for production-ready code.

#!/usr/bin/env python3
"""Wrap a language server and report exit code, wall time, and peak child RSS."""
import resource
import subprocess
import sys
import time

start = time.monotonic()
proc = subprocess.Popen(sys.argv[1:])
code = proc.wait()
elapsed = time.monotonic() - start

# ru_maxrss is kibibytes on Linux and bytes on macOS.
peak_mib = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss / 1024
print(f"exit={code} wall={elapsed:.1f}s peak_rss={peak_mib:.0f}MiB")

# Output shape:
# exit=0 wall=94.3s peak_rss=1382MiB
# Note: RUSAGE_CHILDREN covers every child of this process, so run one server
# per invocation. It only sees children you spawned, which is why the wrapper
# itself has to be the process your editor launches.

Point your client at the wrapper instead of the server binary and the editor launches the wrapper as its child, which makes the server a grandchild the wrapper can account for. Run the same wrapper against both servers on the same workspace, then compare peak RSS and wall time side by side. A server that peaks lower but takes three times as long to answer the first hover is a bad trade on a laptop you close and reopen all day.

The rest of the checklist is short. Pin the server version in your dotfiles so an update does not silently change memory behavior. Keep both servers installed rather than removing the mainstream one. Record the baseline numbers with the date, since both projects move. And re-run the probe after every upgrade, because a capability that disappears between versions is easy to miss and hard to attribute later.

What This Means for Rust Tooling

The interesting part of Rust Glancer is that the LSP boundary makes the choice per workspace instead of per editor, so memory can be tuned like any other resource. The measurement work is the same either way: sample VmRSS and PSS during a rename, wrap the process to catch the index peak, and probe the capability map before you trust the switch. Developers running 32 GB workstations can ignore all of it. Everyone else now has a third option besides closing tabs.

More in-depth coverage from this blog on closely related topics:

Sources and References

Sources cited while researching and writing this article:

Thomas A. Anderson

Mass-produced in late 2022, upgraded frequently. Has opinions about Kubernetes that he formed in roughly 0.3 seconds. Occasionally flops, but don't we all? The One with AI can dodge the bullets easily; it's like one ring to rule them all... sort of...