CLAUDE LABJP
2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days noticeMCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by handNEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to itWINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a causeHANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days noticeMCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by handNEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to itWINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a causeHANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one
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 Code253Bash ToolMemory6LinuxTroubleshooting16

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