VCode home

Memory limits and parking idle tabs

Agents: claude, codex, opencode, cursor · On: desktop, phone

The v-code.service unit runs inside a cgroup with a hard memory ceiling. The tabs share a group of their own below it, tabs/, which carries the soft limit and a hard stop 1 GB under the unit's; the server sits outside that group, so tabs under memory pressure are throttled or killed without it. Each tab gets a cgroup of its own inside tabs/, with its own ceiling: 8 GB, or the tabs' soft limit (2 GB under the unit's MemoryMax) when that is not above 8 GB. Near the unit's ceiling the server refuses to open another tab, and under real pressure it stops idle claude processes so the kernel does not have to.

Why you would use it

Every agent and its children run inside this one unit's cgroup. A test run with 27 workers inside a tab can take the whole desktop down. The unit's cap keeps the kill inside VCode, and the tab's cap keeps it inside the tab that ran the command: the other tabs, and the server, carry on.

How to use it

  1. Check what is set: systemctl --user show v-code -p MemoryHigh -p MemoryMax -p MemorySwapMax. MemoryHigh should read infinity.
  2. Raise the ceiling persistently on a machine with room: systemctl --user set-property v-code MemoryMax=18G
  3. The ceiling applies immediately, and the server re-reads the cgroup per request, so the refusal to open a tab near the ceiling follows it at once. The tabs' group's limits are written at boot: restart the service from a terminal outside VCode, systemctl --user restart v-code, for them to follow.
  4. To change the per-tab cap, set VCODE_TAB_MEMORY_MAX in ~/.config/v-code/env (for example VCODE_TAB_MEMORY_MAX=12G, or 0 for no per-tab cap) and restart the same way. A per-tab cap at or above the tabs' soft limit (2 GB under the unit's MemoryMax) is lowered at boot to that, and the boot line says so. If that would leave a tab under 1 GB, the tabs get no cgroups of their own and the boot says tab cgroups off: unit limit <bytes> leaves under 1G per tab.

Do not set MemoryHigh on the unit. It throttles every process in the unit, the server included: with the tabs past it the server stops answering and the app shows the box offline. One set by hand before is removed in two steps, because it can live in two places: run systemctl --user set-property v-code MemoryHigh=infinity, which clears the one the step 2 of an older version of this page wrote, then delete any MemoryHigh= line from the files in ~/.config/systemd/user/v-code.service.d/ (a drop-in you wrote yourself, which outranks the first step at the next reload) and run systemctl --user daemon-reload. While one stands, the journal says unit memory.high <bytes> is set: it throttles the server with the tabs; remove MemoryHigh from the unit's drop-ins at every start, and the tabs' limits sit under it instead. The soft limit lives on the tabs' group, 2 GB under the unit's ceiling, and needs no setting.

What you see

At boot the journal says whether the tabs got their own cgroups: tab cgroups on: /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/v-code.service, memory.max 8589934592 per tab, memory.swap.max 536870912, tabs memory.high 10737418240, memory.max 11811160064 (; VCODE_TAB_MEMORY_MAX 8589934592 lowered to the tabs' memory.high 7516192768 (the unit's memory.max 9663676416 less 2G) at the end when the cap was lowered; no swap accounting in place of memory.swap.max 536870912 on a kernel booted without it; tabs no limit for a unit with no MemoryMax), or tab cgroups off: <reason>. For example tab cgroups off: v-code.service is not delegated (Delegate=yes) on a unit rendered before this change, until install.sh update re-renders it, or tab cgroups off: v-code.service: systemctl show failed: ENOENT when systemctl could not be asked (ETIMEDOUT, or exit <code>, for the other ways it fails).

When a command in a tab passes the tab's cap, the kernel kills the largest process in that tab, and nothing outside the tab. Which process that is depends on what is running:

The kernel log names the kill: journalctl -k | grep -i 'memory cgroup out of memory'.

Under pressure the journal also names the three tabs holding the most, at most once every 10 minutes. A tab's figure leaves out page cache nothing is using: memory pressure: largest tabs <thread-id> 6.5 GB, <thread-id> 2.0 GB, <thread-id> 1.0 GB.

The journal logs one line per parked tab: parked <thread-id> after 4 min idle, and parked idle claude process from the runner. The 503 a user meets when the cap is close, and what a parked tab feels like from the phone, are in memory-headroom-and-idle-parking.

Options and settings

Option Default What it changes
MemoryMax 12G The hard ceiling for the whole instance
MemoryHigh not set Leave it unset: on the unit it throttles the server with the tabs
Tabs' soft limit MemoryMax minus 2 GB memory.high on tabs/: where reclaim and throttling begin, for the tabs alone. Written at boot
Tabs' hard stop MemoryMax minus 1 GB memory.max on tabs/: the OOM kill lands inside the tabs, not on a server under 1 GB. Written at boot
MemorySwapMax 2G How much swap the unit may use
OOMPolicy continue An agent child killed by the OOM killer does not take VCode with it
Delegate yes Lets the server make a cgroup per tab below the unit
DelegateSubgroup server systemd 254 and later start the server in server/ below the unit. Older systemd warns about an unknown setting and ignores it; the server moves itself there
VCODE_TAB_MEMORY_MAX 8G Each tab's own ceiling: bytes, or a whole number with K, M or G, at least 256M. 0 is no per-tab cap. Anything else fails the boot. Lowered to the tabs' soft limit, 2G under the unit's MemoryMax, when it is not below it; off if that leaves under 1G
Tab swap 512M Each tab's memory.swap.max, so a tab at its cap cannot take the unit's whole MemorySwapMax before the kill. A constant (TAB_SWAP_MAX in lib/tab-cgroup.js)

The guard's own thresholds (new-tab headroom, parking headroom, idle time, reaper interval, CPU mark) are constants in lib/memory-guard.js and are listed in memory-headroom-and-idle-parking.

Limits and known gaps