blog.rust.gay

rust-analyzer memory usage in large workspaces

rust-analyzer keeps an in-memory model of your project so it can answer queries like completion, go-to-definition, and diagnostics without re-running a full compiler pass for every keystroke. In a large Cargo workspace, that model includes dependency metadata, parsed syntax, macro expansion results, and caches warmed during startup. The memory cost depends on what rust-analyzer is asked to analyze, not just on the number of files in the editor.

There is no single “normal” number. A small crate and a multi-crate monorepo with many features, targets, build scripts, and procedural macros can have very different profiles. The useful question is usually: which parts of the analysis are enabled, and which of those can I narrow without losing the IDE behavior I need?

What drives memory use

The biggest multipliers are scope and code generation:

  • Workspace and dependency scope. rust-analyzer loads project structure from Cargo metadata and analyzes configured crates. rust-analyzer.cargo.allTargets defaults to true, which passes --all-targets to Cargo invocations. That is convenient, but it means tests, examples, benches, and other targets can be part of the analyzed picture.
  • Build scripts and procedural macros. rust-analyzer.cargo.buildScripts.enable defaults to true, and rust-analyzer.procMacro.enable defaults to true (enabling proc-macro support implies build-script support). Running build scripts and expanding proc macros gives more precise analysis, but it adds work and state. The proc-macro server can run in parallel processes; rust-analyzer.procMacro.processes defaults to 2.
  • Cache priming. rust-analyzer.cachePriming.enable defaults to true. Cache priming warms caches on project load so early IDE requests are faster, at the cost of extra CPU and memory pressure during startup.
  • Feature and target selection. rust-analyzer.cargo.features, rust-analyzer.cargo.noDefaultFeatures, and rust-analyzer.cargo.target control what Cargo resolves. A broader feature set or additional targets can expand the code rust-analyzer has to understand.

The 2020 rust-analyzer blog post on measuring memory usage is useful background for how the project reasons about heap size, but it is not a current benchmark for modern rust-analyzer releases.

Narrow the analysis before turning features off

Start by reducing accidental scope. These settings keep analysis closer to the code you actually work on:

{
  "cargo": {
    "allTargets": false,
    "features": ["my-feature"],
    "noDefaultFeatures": false,
    "target": "x86_64-unknown-linux-gnu"
  }
}

cargo.features can also be set to "all"; that is the opposite of narrowing. If a workspace only needs one platform or a small feature matrix in the editor, pinning cargo.target and an explicit feature list is often the least destructive change.

For diagnostics on save, rust-analyzer.checkOnSave defaults to true. The check command is configurable: rust-analyzer.check.command defaults to "check", and check-specific settings such as check.allTargets, check.features, check.targets, and check.extraArgs can differ from the Cargo load settings. If save-time checks are expensive in a large workspace, tune them deliberately instead of guessing — disabling checkOnSave removes on-save Cargo diagnostics, which is a real trade-off.

rust-analyzer.files.exclude can keep rust-analyzer from watching or paying attention to selected paths relative to the workspace root (globs are not supported, and editor file watchers may need matching configuration). This is useful for generated output, vendored snapshots, or large non-source trees that happen to live in the workspace.

Proc macros, build scripts, and startup caches

When macro-heavy crates dominate memory or startup time, the relevant knobs are explicit:

{
  "cachePriming": {
    "enable": true,
    "numThreads": "physical"
  },
  "cargo": {
    "buildScripts": {
      "enable": true
    }
  },
  "procMacro": {
    "enable": true,
    "processes": 2
  }
}

Turning procMacro.enable or cargo.buildScripts.enable off can reduce work, but it also reduces accuracy for code that depends on generated items or macro expansions. That is sometimes acceptable in a narrow editing session and harmful in a workspace that relies heavily on derives or build-time code generation. Treat these as precision/performance controls, not generic “make it faster” switches.

cachePriming.numThreads defaults to "physical"; 0 means automatic selection. Lowering concurrency may reduce peak startup pressure on constrained machines, but it can make the editor slower to become fully responsive.

For advanced tuning, the manual also documents rust-analyzer.lru.capacity (syntax trees kept in memory, default 128) and rust-analyzer.numThreads. These are not the first knobs to reach for; change them when you have a measured reason.

Diagnose with data, not guesses

If rust-analyzer competes with manual Cargo commands for the build lock, rust-analyzer.cargo.targetDir can give rust-analyzer its own target directory. The FAQ notes this avoids lock contention and cache thrashing at the cost of extra disk space from duplicated artifacts — it is an operational fix, not a direct RAM limit.

When behavior is unclear, the troubleshooting guide recommends checking logs (RA_LOG=info for general logs, project-model logging for load issues) and, if rust-analyzer crashes, running rust-analyzer analysis-stats /path/to/project/directory/ from the command line. In large workspaces, reproducing a problem with a narrow project or a specific target/feature set makes configuration experiments much easier to interpret.

The practical goal is a stable baseline: analyze the targets and features you care about, keep expensive code generation only where it pays for itself, and verify changes against real editor latency and memory behavior.

Sources


← back to posts