guide #021

V8 Semi-Space 2 -> 1 MB

Problem
V8 young generation (semi-space) defaults to 2 MB. Wasted for I/O-bound gateway.

Solution
--max-semi-space-size=1 + --max-old-space-size=150. RSS dropped 216 -> 180 MB (-36 MB).

Context

V8 uses a generational garbage collector. Newly allocated objects land in the "young generation", which is divided into two equal-sized semi-spaces: one active (allocation space) and one idle (used during scavenging). When the active semi-space fills up, V8 performs a minor GC (scavenge): it copies surviving objects to the idle semi-space (or promotes them to old space if they survived a previous scavenge), then swaps the two spaces.

The default semi-space size on 32-bit ARM is 2 MB per space, meaning 4 MB total young generation. For a server-class Node.js app that allocates heavily (React rendering, JSON parsing), this makes sense. But the OpenClaw gateway is primarily I/O-bound: it proxies API calls to LLM providers, manages WebSocket connections, and streams responses. The allocation rate is modest, and most short-lived objects die quickly.

Shrinking the semi-space from 2 MB to 1 MB means minor GCs happen more frequently, but each scavenge is faster (less memory to scan). On a single-core Snapdragon 410, the total GC time stays roughly the same while RSS drops.

Implementation

Add the flag to NODE_OPTIONS in the start script:

# In $PREFIX/bin/start-openclaw:
export NODE_OPTIONS='-r $HIJACK --expose-gc --no-warnings --max-old-space-size=150 --max-semi-space-size=1'

The combined heap tuning settings tested during this phase:

SettingResult
--max-old-space-size=128Instant OOM at startup
--max-old-space-size=140V8 SIGABRT after ~1 hour
--initial-old-space-size=32Crashes immediately (unsupported flag)
--max-old-space-size=150 --max-semi-space-size=1Stable 11h+

The semi-space flag interacts with the old-space cap. Smaller semi-space means more objects get promoted to old space sooner, increasing old-space pressure. The 150 MB old-space cap provides enough headroom for this.

Verification

# Check V8 heap stats from inside the gateway (via --expose-gc):
curl -s http://localhost:9000/api/status | python3 -c "
import sys, json
d = json.load(sys.stdin)
heap = d.get('memory', {})
print(f'Heap used: {heap.get(\"heapUsed\", 0) / 1024 / 1024:.1f} MB')
print(f'Heap total: {heap.get(\"heapTotal\", 0) / 1024 / 1024:.1f} MB')
"

# Check RSS of the gateway process:
ps -o pid,rss,comm -p $(pgrep -f openclaw-gateway)
# Expected: RSS ~180 MB (down from ~216 MB)

# Monitor minor GC frequency (trace GC output):
NODE_OPTIONS='--trace-gc --max-semi-space-size=1' node -e "
  for (let i = 0; i < 10000; i++) Buffer.alloc(1024);
  console.log('done');
" 2>&1 | grep Scavenge | wc -l
# More scavenges than with default, but each is faster

Gotchas

  • Setting --max-semi-space-size=1 causes significantly more frequent minor GCs. In later testing, this was refined to --max-semi-space-size=2 as the sweet spot. At 1 MB, the scavenge frequency on the gateway was roughly 3x higher, which added CPU load during LLM streaming
  • The value is per semi-space, not total. --max-semi-space-size=2 means 4 MB total young generation (2 MB active + 2 MB idle)
  • This flag has no effect if V8 decides to use a larger semi-space internally during startup. The flag sets the maximum, not the exact size
  • On the native node22-icu build (Hack #12), the final tuned value became --max-semi-space-size=2 with --max-old-space-size=112. The 150 MB old-space cap from this hack was later reduced as heap profiling improved
  • Do not confuse with --max-old-space-size. Semi-space controls young generation only

Result

MetricDefault (2 MB)1 MB2 MB (final)
Young gen total4 MB2 MB4 MB
Minor GC frequencybaseline~3x more~1.5x more
RSS impact216 MB180 MB~183 MB
Stabilitystablestable 11h+stable (production)

The --max-semi-space-size=1 setting was the first version deployed and proved stable for 11+ hours. It was later relaxed to 2 to reduce minor GC churn on the single-core CPU. Combined with --max-old-space-size=112 on the native build, the final production NODE_OPTIONS line became:

export NODE_OPTIONS='-r $HIJACK --expose-gc --no-warnings --max-old-space-size=112 --max-semi-space-size=2'
Continue reading
guide
Pocket AI complete guide
Running self-hosted AI on portable hardware
guide
Edge AI hardware buyer's guide 2026
Pi 5 vs Mini PC vs Mac Mini
report
Self-hosted AI landscape 2026
Quarterly state of the ecosystem
section
Pocket AI hardware hub
All portable hosts reviewed
section
Agent tracker
Live stats on every agent
newsletter
Thursday digest
Weekly summary in your inbox