◉CLAUDE LABJP
●2.1.285 — Claude Code 2.1.285 (Sep 29): set CLAUDE_CODE_DISABLE_WEB_FETCH to turn off WebFetch, and run claude --desktop to open the current directory in the desktop app●SONNET5.5 — With 2.1.284, claude-sonnet-5-5 is the default Sonnet on the API: 1M context, $2 / $10 per Mtok, $0.20 cache reads●10/07 — 7 days left until the old spellings of Claude Desktop / Cowork managed-config keys stop being accepted. After the cutoff they fail closed●WINDOWS — Two Windows questions keep coming up in Issues: the desktop app won't relaunch, and it stays on top of every other window. Check for an orphaned process before touching settings●NEW — The stable Claude Code channel still hasn't received two default-model changes●BING — 1,055 Bing sessions in 28 days, up four weeks running. Readers land on billing errors, the Academic plan, Figma MCP and PowerShell●2.1.285 — Claude Code 2.1.285 (Sep 29): set CLAUDE_CODE_DISABLE_WEB_FETCH to turn off WebFetch, and run claude --desktop to open the current directory in the desktop app●SONNET5.5 — With 2.1.284, claude-sonnet-5-5 is the default Sonnet on the API: 1M context, $2 / $10 per Mtok, $0.20 cache reads●10/07 — 7 days left until the old spellings of Claude Desktop / Cowork managed-config keys stop being accepted. After the cutoff they fail closed●WINDOWS — Two Windows questions keep coming up in Issues: the desktop app won't relaunch, and it stays on top of every other window. Check for an orphaned process before touching settings●NEW — The stable Claude Code channel still hasn't received two default-model changes●BING — 1,055 Bing sessions in 28 days, up four weeks running. Readers land on billing errors, the Academic plan, Figma MCP and PowerShell
Articles/Claude Code
⟐ Claude Code/2026-08-16Intermediate

A Runaway Build Dies Very Differently Under cgroup Than Under ulimit

Claude Code v2.1.233 added opt-in memory cgroup support for Bash tool commands on Linux. Capping virtual address space with ulimit -v and capping physical memory with a cgroup produce completely different failures — and only one of them lets your Node build start at all.

Claude Code258Bash ToolMemory6LinuxTroubleshooting17

An unattended job on my machine once ended partway through a build, quietly.

Disk space was my first suspect. As an indie developer I keep several repositories — App Store side projects and the sites I publish — on one machine, so ENOSPC is not a rare accident here. But there was plenty of room left that time, so the culprit was somewhere else.

Memory was next on the list. When a build grabs a few gigabytes for a moment, it takes the working session on the same machine down with it.

The Claude Code v2.1.233 changelog mentions CLAUDE_CODE_TOOL_MEMORY_LIMIT, which puts Bash tool commands into a Linux memory cgroup so that, in its words, a runaway build cannot stall the session.

So where should the ceiling sit on my own machine? And does a ceiling actually protect the session? I spent an afternoon on Ubuntu 22.04 (kernel 6.8) finding out, and the answer turned out to depend almost entirely on where you put the ceiling.

Capping address space is not the same as capping memory

Linux gives you two broad ways to constrain a process, and they behave nothing alike.

MechanismWhat it capsWhat happens at the limitScope
ulimit -v (RLIMIT_AS)Virtual address space reservationsThe allocation call fails; the process stays aliveThe shell and its children
cgroup v2 memory.maxPhysical memory actually in useIf reclaim fails, the OOM killer sends SIGKILLEvery process in the cgroup

The first mechanism politely declines a request. The second one kills you for using too much.

What a program does after being declined is entirely up to that program — and that is where the interesting part lives.

Under ulimit -v, Python failed politely

I started with a Python process that grabs memory in small pieces.

( ulimit -v 524288; python3 -c "
b = []
n = 0
try:
    while True:
        b.append(bytearray(4 * 1024 * 1024))
        n += 4
except MemoryError:
    print(n)
" )

ulimit -v takes KiB, so 524288 is 512 MiB. Three runs produced the same number:

492
492
492

Of the 512 MiB ceiling, 492 MiB was usable. The missing ~20 MiB is what the interpreter itself had already reserved before my loop ran at all. Repeating the measurement at other ceilings showed that overhead staying roughly constant:

ulimit -vAllocated before MemoryErrorOverhead
256 MiB232 MiB~24 MiB
512 MiB492 MiB~20 MiB
1024 MiB1000 MiB~24 MiB

That constant matters when you are picking a number. The ceiling is not a budget for your workload; it is a budget for your workload plus whatever the runtime reserved on the way in. On a small ceiling that overhead is a tenth of everything you have.

The number matters less than the shape of the failure: the process caught a plain MemoryError, cleaned up after itself, and chose its own exit code. Nothing was killed.

For a Python script you control, ulimit -v is a reasonable way to manufacture a well-behaved failure.

Under the same cap, Node never even started

Applying that same 512 MiB to Node.js (v22.22.3) went very differently.

( ulimit -v 524288; node -e "console.log('started')" )

No started. Exit code 133. The first meaningful line on stderr:

# Fatal process out of memory: Failed to reserve virtual memory for CodeRange

No try / catch was involved. V8 reserves a large slab of virtual address space during startup, so the process died before any build code ran.

I bisected to find the boundary:

ulimit -vExit code of node -e "0"
512 MiB133 (startup fails)
640 MiB133 (startup fails)
704 MiB133 (startup fails)
768 MiB0 (starts)
896 MiB0 (starts)

On this machine, 768 MiB was the floor just to run an empty script.

Raise the ceiling to 2 GiB and runtime allocation failures do become catchable:

caught@1056MiB Array buffer allocation failed

Only the startup reservation is fatal. Everything after it behaves like an ordinary exception.

The practical conclusion is a strong one. Putting a low ulimit -v on a Node toolchain — npm, Vite, webpack, Metro — does not tame runaway builds. It kills healthy builds at startup instead. "Let's just cap the build at 512 MB" is not a plan that survives contact with V8.

Seen in that light, the decision to build CLAUDE_CODE_TOOL_MEMORY_LIMIT on cgroups rather than rlimits reads as the obvious one.

Under a cgroup, failure arrives as SIGKILL

cgroup v2's memory.max watches physical memory. Virtual reservations pass straight through, so Node starts normally, and the limit only bites when memory is genuinely in use.

At that point the kernel sends SIGKILL. It cannot be caught. The shell reports exit code 137 (128 + 9).

bash -c 'sleep 20 & P=$!; kill -9 $P; wait $P'
echo $?
137

For anyone running unattended work, that difference is mostly a difference in how you read logs:

  • Exit code 137 means look at memory first.
  • An instant death like 133 points at address space limits or container configuration.
  • An exception such as MemoryError in the log means the process survived long enough to explain itself.

Retry logic changes too. A SIGKILLed build leaves nothing behind, so retrying it unchanged simply gets it killed at the same point. Raising the ceiling or lowering concurrency has to come first.

It is also worth knowing that a cgroup does not kill on the first byte over the line. The kernel tries to reclaim memory first — page cache, clean pages, anything it can drop — and only escalates when reclaim cannot keep up. In practice that means a build sitting just under the ceiling gets slower rather than killed, because the cache it was relying on keeps getting evicted. A build that looks mysteriously sluggish under a limit is often telling you the ceiling is a little too tight rather than fine.

One honest caveat about my setup: the sandbox runs as an unprivileged user, so creating a directory under /sys/fs/cgroup returns Permission denied, and systemd-run --user has no user bus to talk to. I could confirm the exit-code convention but not exercise the cgroup path itself. On your own machine, the reliable test is to point a disposable build at something like systemd-run --user --scope -p MemoryMax=2G -- npm run build and let it get killed once on purpose.

One more note, accurate as of August 16, 2026: CLAUDE_CODE_TOOL_MEMORY_LIMIT appears in the changelog but is not yet listed on the environment variables reference page. The accepted value format may change, so check the primary source before you set it — which is why this article does not assert one.

Two decisions worth making before you set a ceiling

Once you decide to impose a limit, only two things really need deciding.

First, the number itself. Measure what one healthy build actually uses, then start at 1.5 to 2 times that figure. Then multiply by the number of builds that can run at once and confirm the total stays under your physical memory. In setups with several unattended jobs, skipping that multiplication is how you end up with limits in place and a machine that still crawls. Staggering start times is often cheaper than lowering every ceiling, since two heavy builds rarely need to overlap.

Second, what failure should look like. Decide where an exit code of 137 gets recorded and how it surfaces. Without that decision, unattended work produces mornings where something "just ended." Stopping silent failures in general is covered in the run that logged success while producing nothing, and if you are isolating Bash tool errors more broadly, Claude Code Bash tool execution errors is the closer companion piece.

If you do one thing today, run your heaviest build once under /usr/bin/time -v and write down Maximum resident set size. Every argument about the right ceiling starts from that single line.

I had been watching disk space and never measuring memory. If you have the same gap, I hope this saves you a confusing morning.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-09-10
When Every Request Fails With Not signed in to the Cloud gateway, Check the Version Before the Config
A Claude Code 2.1.265 regression broke every request for gateway and proxy setups. I use it as an excuse to build a small snapshot of the auth input surface, so the next time something breaks you can tell config from version in seconds.
⟐ Claude Code2026-09-04
The ~/.claude.json Rollback Is Fixed in v2.1.259. Putting Back What It Erased Is Still Your Job
Concurrent sessions used to silently roll back each other's ~/.claude.json changes. v2.1.259 fixed that, but nothing restores the trust settings and MCP servers you already lost. Here is how I find and rebuild them, with a small key-path diff script.
⟐ Claude Code2026-09-01
Drop the quotes on a heredoc and the prices in your log quietly change
An unquoted heredoc runs the variables and backticks inside its body. Here are the four ways my log got rewritten, how the three quoting forms compare, a safe placeholder-and-sed pattern, and a small script for auditing what you already have.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links