Skip to content

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

vscode-memory-watch

Find out which VS Code process is eating your RAM, before the crash destroys the evidence.

VS Code is a tree of 30–50 processes that mostly share one binary and one name. When the window balloons to 15–20 GB and you reload it to stay sane, you learn nothing: the process that was leaking is gone. This samples the tree on an interval and records memory per tier, so the tier that climbs names itself, and it captures a full per-process snapshot at the moment things go bad.

Four hours of sampling pinned a months-old "VS Code is eating 20 GB" problem to a single webview process — independently confirming an upstream bug. See What it found.

Usage

$ vscode-memory-watch --once      # one reading, right now
VS Code tree: 9589 MB PSS (12622 MB RSS) across 38 processes, 9 Claude session(s)
  renderer=4942MB exthost=497MB claude=2417MB tsserver=346MB langserver=350MB helper=191MB nodeutil=567MB utility=21MB gpu=83MB main=155MB zygote=13MB
  largest: renderer pid=3369519 at 4360 MB
  (4 integrated-terminal process(es) excluded)

  4465437 KB pss    4576384 KB rss  renderer   pid=3369519  window
   439848 KB pss     513072 KB rss  nodeutil   pid=3369526
   336143 KB pss     458388 KB rss  claude     pid=3380222  session=e6e314ca-f038-44a1-8966-6bed829acd46
   326732 KB pss     447744 KB rss  claude     pid=3372538  session=55322f72-2988-4235-a7b2-20b35822b51a
   ...

vscode-memory-watch --report prints the same tiers as a growth table — start, now, peak and delta for each — since the current run started (an editor restart or a window reload begins a new run). That's the view that identifies a leak, and there's a real one in What it found.

Run with no arguments to sample continuously. --help lists everything.

Install

install -Dm755 vscode-memory-watch ~/.local/bin/vscode-memory-watch
install -Dm644 vscode-memory-watch.service ~/.config/systemd/user/vscode-memory-watch.service
systemctl --user daemon-reload
systemctl --user enable --now vscode-memory-watch.service

Needs bash 4.2+ (for printf '%()T'), awk, coreutils, getconf and Linux /proc. notify-send is optional — without it the snapshot is still written, you just don't get the desktop alert.

How it finds the tree

It finds every renderer, walks up to the process that owns it, and then walks back down through the tree. Renderers are the anchor because a renderer is the one thing only a real editor instance has. That matters more than it sounds: a code --wait used as your git editor, or a code --status, spawns Chromium children of its own and matches every command-line pattern a real main process does. Identifying roots by pattern instead adopts those as extra instances, and since a change in the set of main processes is what ends a run, a routine git commit would silently discard hours of accumulated growth curve.

The owner it walks up to must itself look like an editor — named like one, or carrying a VS Code entry point in its command line. Every Electron app has renderers, and without that check a Signal or Obsidian running on the system electron binary would be adopted as a "VS Code" instance, merged into every total, and its exit would end the recorded run.

Working from the tree rather than from install paths is also what keeps it working across VS Code, Code - OSS, VSCodium, Cursor, Windsurf and code-server, none of which agree on where they live or what they call their user-data directory.

The walk is bounded. It descends through VS Code's own process types and stops at anything else, and it skips any process with a controlling terminal — except the terminal the editor itself was launched from, so a code or code-server started from a shell is still found. An extension's language server is counted, but the cargo build you started in the integrated terminal is not — that is your memory, not VS Code's, and sweeping it in would both inflate the headline and fire alerts naming the wrong thing. Excluded terminal processes are counted and reported so you know they were left out.

Processes are then classified into tiers by command line, because every Chromium tier is the same binary under the same name:

tier what it is
renderer window renderers and out-of-process webview iframes
exthost extension hosts
nodeutil other Node utility processes — shared process, pty host, file watcher
claude Claude Code session processes (one per open session)
tsserver, langserver TypeScript server and language servers
helper anything else an extension spawned: tool servers, language runtimes
utility, gpu, main, zygote the rest of the Electron tree

Splitting exthost from nodeutil is not cosmetic: the extension host, shared process, pty host and file watcher are all --type=utility --utility-sub-type=node.mojom.NodeService with near-identical command lines. Only the extension host is started with a debug port, which is what separates it here. Without that split, a pty-host leak gets reported as extension-host growth.

Why PSS

It reports PSS, not RSS. RSS counts every shared mapping once per process, so summing it across a 40-process Electron tree overstates real usage badly — 12622 MB RSS against 9589 MB PSS in the example above. PSS divides each shared page among the processes mapping it, so the tiers actually add up to the total.

Where smaps_rollup can't be read — a process exiting mid-sample, or a kernel built without CONFIG_PROC_PAGE_MONITOR — it falls back to RSS, marks that figure (rss estimate), and counts it in a pss_fallback column, rather than quietly reporting RSS under a PSS heading.

Cost

A sample costs about 0.7 s of CPU on a 40-process tree — roughly 1% of one core at the 60 s default. Almost all of it is the kernel: reading smaps_rollup makes it walk a process's page tables, which for a 4 GB renderer is genuinely expensive. That is the price of PSS, and there is no cheaper way to get an honest number. If it matters on your machine, raise VSCODE_MEM_INTERVAL.

What the script itself does is cheap and deliberately kept that way: per-process work is pure bash reading /proc, with no forks or subshells, and the per-process detail table is only sorted when something is actually going to read it — not on the ~1,400 daily samples that produce no output. The service ships with Nice=10 and IOSchedulingClass=idle so sampling never competes with the thing it is measuring.

It walks /proc directly rather than using pgrep -f, whose pattern would match the watcher's own command line. The bounded walk keeps that property: the watcher runs from systemd, outside the tree, and even run by hand from a VS Code terminal it is excluded along with the rest of the terminal's processes.

Alerting

When the tree crosses 12000 MB PSS, or any single process crosses 4000 MB, it sends a desktop notification and writes a full per-process snapshot to snapshots/.

One snapshot file per run, rewritten on every new high — not just the first crossing, because the notification cooldown would otherwise freeze the evidence at whatever the tree looked like when it first crossed the line, which is not the peak and not what you want to paste into a bug report. The peak tracker resets when the run does, so day two's smaller-but-still-huge leak isn't silenced by day one's record. Notifications themselves stay rate-limited to one per 30 minutes, and that limit persists across restarts so a crash-looping service can't spam you.

variable default meaning
VSCODE_MEM_INTERVAL 60 seconds between samples
VSCODE_MEM_WARN_TOTAL 12000 whole-tree PSS warning line, MB
VSCODE_MEM_WARN_PROC 4000 single-process PSS warning line, MB
VSCODE_MEM_COOLDOWN 1800 seconds between notifications

History file

Samples land in ~/.local/state/vscode-memory-watch/history.csv, one row per sample, with a column per tier. The header is generated from the tier list, so the columns can't drift from the data, and --report looks columns up by name rather than position.

If the tier list ever changes, the old log is moved aside automatically rather than having new rows appended under a header that no longer describes them — a mismatched row reads as another tier's numbers, silently.

--report describes the current run only. A run ends when the heap under observation was thrown away, which happens two ways: the editor restarts (the set of main processes changes), or you reload the window (the renderer tier collapses while the main process carries on — the more common case by far, and the one a main-PID check alone would miss). Reloads are counted and shown.

It deliberately does not split on a gap in the log: a laptop that sleeps overnight leaves a gap, but the memory survives sleep and the run genuinely continues. Gaps are excluded from the growth-rate denominator instead, and reported. The sampling cadence used for that is the modal gap between samples, measured from the data — not the smallest gap, which one quick daemon restart would poison, and not your environment, which would let the shell you happen to run --report from change the headline growth rate by a multiple.

What it found

I wrote this because VS Code was reaching ~20 GB every couple of days and I was tired of guessing. Four hours of sampling gave an unambiguous answer:

$ vscode-memory-watch --report
current run: 2026-08-11T18:45:37-04:00 -> 2026-08-11T22:42:01-04:00 (238 samples, ~238 min)

tier              start        now       peak     growth
total_pss        3829 MB     8410 MB     9634 MB    +4581 MB
total_rss        5919 MB    11131 MB    12357 MB    +5212 MB
claude           1030 MB     2163 MB     2234 MB    +1133 MB
exthost          1063 MB     1124 MB     2388 MB      +61 MB
renderer          924 MB     4096 MB     5317 MB    +3172 MB
tsserver          369 MB      490 MB     1784 MB     +121 MB
langserver        176 MB      285 MB      285 MB     +109 MB
gpu                77 MB       80 MB       81 MB       +3 MB
main              148 MB      148 MB      152 MB       +0 MB
other              40 MB       21 MB       40 MB      -19 MB

processes: 30 -> 31   claude sessions: 4 -> 8
tree growth rate: +1155 MB/hour

(Verbatim v1.0 output — the version running during the investigation; the script shipped here is v2.0.0, which splits nodeutil out of exthost and helper out of other, among other fixes. The excerpt covers the first 238 of the 296 samples in examples/history-incident-v1.csv — the log continues another hour with the same pattern.)

The renderer tier grew 924 MB → 5317 MB (~1.1 GB/hour), and the growth was one process: the CSV's top-process column shows the webview renderer climbing 503 MB → 4796 MB while the editor windows' own renderers stayed in the 250–320 MB range. Every other tier either scaled with session count (claude, 4 → 8 tabs) or sawtoothed normally under garbage collection: the extension host netted +61 MB over the window despite peaking at 2388 MB, and tsserver peaked at 1784 MB and came back down to 490 MB. In the snapshot captured as the tree crossed the alert line (examples/snapshot-incident-v1.txt), that one renderer held 4097 MB PSS while the next largest process in the tree held 486 MB.

That matches anthropics/claude-code#84013, reported upstream independently a week earlier: the Claude Code conversation webview retains all rendered history, and neither /compact nor closing session tabs releases it.

The per-tier split is the whole point. "VS Code is using 20 GB" is not actionable. "One webview renderer is using 4.1 GB while the next largest process is 486 MB, and the extension host holding the same conversation data nets +61 MB over four hours" is a bug report.

examples/ has the raw samples, the snapshot captured during the incident, and current-version output.

License

MIT

About

Find out which VS Code process is eating your RAM. Per-tier PSS sampling of the Electron process tree, with a snapshot captured at peak.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages