CLAUDE LABJP
2.1.269 — Prompt suggestions were being dropped for Japanese, Chinese, Thai and other languages written without spaces between words. That is now fixedKB5124008 — After September's cumulative update, Windows 11 users report Cowork failing to mount any host folder at all. The VM still starts, which makes the cause hard to pin downPUSH — Some cloud and Cowork sessions have git push rejected by the proxy before it reaches GitHub. Cloning still works, so it reads like a token permission problem when it is notNEW — Adding more material made Projects answer thinner. Three questions we now use to decide which knowledge files stayTOKENS — You cannot price a PDF before sending it: CountTokens does not accept document input. That leaves estimating from page count or extracting the text and counting thatCLEANUP — Before asking an agent to tidy up, separate the work that only needs reading from the work that needs writing. The order you hand over folders cannot be reconsidered afterwards2.1.269 — Prompt suggestions were being dropped for Japanese, Chinese, Thai and other languages written without spaces between words. That is now fixedKB5124008 — After September's cumulative update, Windows 11 users report Cowork failing to mount any host folder at all. The VM still starts, which makes the cause hard to pin downPUSH — Some cloud and Cowork sessions have git push rejected by the proxy before it reaches GitHub. Cloning still works, so it reads like a token permission problem when it is notNEW — Adding more material made Projects answer thinner. Three questions we now use to decide which knowledge files stayTOKENS — You cannot price a PDF before sending it: CountTokens does not accept document input. That leaves estimating from page count or extracting the text and counting thatCLEANUP — Before asking an agent to tidy up, separate the work that only needs reading from the work that needs writing. The order you hand over folders cannot be reconsidered afterwards
Articles/API & SDK
API & SDK/2026-07-31Advanced

The 400-Character Preview That Was Still Holding On to a Megabyte

Truncating tool output in your own MCP server does not free the original. Node's sliced strings keep the parent alive. Here is the measured 13-character boundary, which flattening tricks actually work, and a heap-snapshot audit script that counts the retained parents for you.

MCP53Node.js4Memory6Performance4Operations18

Premium Article

I wrote a very small MCP server whose only job was to read my own build logs. One tool, one file read, one string returned. To keep the return value manageable I stored only the first 400 characters as a preview and left the full text to be re-read on demand.

As an indie developer running several sites at once, I accumulate little resident processes like this one at a steady drip. Each was supposed to cost me a few dozen megabytes.

A few hours into running it, ps stopped me cold. The resident set of that little process had crossed 200 MB.

The history array was supposed to hold nothing but 400-character previews. Hundreds of them still add up to tens of kilobytes. The memory did not come down.

I spent a long time looking in the wrong places: unconsumed streams, Buffer handles I had forgotten to release, event listeners piling up on a long-lived emitter. None of it.

The culprit was the single line of slice I had written to do the truncating.

A 400-character preview that will not let go of its parent

Start with the smallest reproduction I could build. Create a 4 MB string twenty times, keep only the first 200 characters of each.

// exp1.mjs — node --expose-gc exp1.mjs
function mb(n) { return (n / 1048576).toFixed(1); }
 
const KEEP = 200;
const N = 20;
const SIZE = 4 * 1024 * 1024;
const kept = [];
 
global.gc();
const base = process.memoryUsage().heapUsed;
 
for (let i = 0; i < N; i++) {
  const full = 'x'.repeat(SIZE) + '#' + i;  // stand-in for a big tool result
  kept.push(full.slice(0, KEEP));            // "keep only 200 characters"
}
 
global.gc();
const after = process.memoryUsage().heapUsed;
 
console.log('kept =', kept.length,
            '/ previewLen =', kept[0].length,
            '/ heapDelta =', mb(after - base), 'MB');

The explicit global.gc() calls are there so the result does not depend on when a collection happens to fire. They require the --expose-gc flag.

Every number below comes from Node.js v22.22.3 (V8 12.4.254.21) on Linux x86_64, 4 vCPU, 3.9 GB of RAM, inside a sandbox.

kept = 20 / previewLen = 200 / heapDelta = 80.0 MB

Twenty strings of 200 characters. Four thousand characters in total. Eighty megabytes unreclaimed — exactly 4 MB times 20.

All twenty parents are still alive.

The boundary sits at exactly 13 characters

The obvious follow-up question is how short a preview has to be before it stops pinning the parent. Same script, varying only KEEP, this time with 2 MB parents and ten iterations.

Preview length (chars)UnreclaimedReading
12.00 MBparent freed
52.00 MBparent freed
112.00 MBparent freed
122.00 MBstill safe
1320.00 MBparent pinned
1420.00 MBparent pinned
20020.00 MBparent pinned

Between 12 and 13, the number jumps by an order of magnitude.

The residual 2.00 MB is the last full still being reachable when the measurement runs. It stayed at 2.00 MB whether I looped once or forty times, so I treated it as a constant floor. Above the threshold, the growth is strictly linear.

CallsUnreclaimed (200-char preview, 4 MB parent)
14.00 MB
28.00 MB
520.00 MB
1040.00 MB
2080.00 MB
40160.00 MB

A perfectly straight line. The longer the process runs, the worse it gets.

That 13 lines up with the minimum length at which V8 switches to representing a substring as a reference to its parent plus an offset and a length — a sliced string. Anything shorter is simply copied on the spot, which is why the parent goes away.

Note how that inverts the usual intuition: shorter is safer, but only below a threshold too small to carry any useful information. Trimming your previews to 12 characters technically fixes the leak and destroys the feature.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
The measured 13-character threshold where a preview starts pinning the entire parent string alive (4 MB becomes 80 MB over 20 calls)
A side-by-side table of 5 flattening techniques that work and 7 that silently do not, including template literals, String(), and repeat(1)
A 200-call tool wrapper benchmark: heapUsed 203.5 MB to 4.6 MB, RSS 245.0 MB to 43.6 MB, for a total cost of 16 milliseconds
A 50-line audit script that walks a heap snapshot, sums the retained parents, and exits 1 when they blow a byte budget
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

API & SDK2026-07-24
Running Four Sites From One Managed Agent Definition
A design for collapsing four near-identical Managed Agents into one base definition, using agent version pinning and session-local overrides — with a validated factory and the traps I hit along the way.
Claude Code2026-07-16
Your Overnight Session Wakes Up at 3GB — Four Places Memory Piles Up, and How to Tell Them Apart
The Claude Code process I left running overnight had grown to 3.4GB of resident memory by morning. Here are the four accumulation sources closed in 2.1.209, how to separate what's left in your own setup by sampling RSS slope, and a watchdog pattern that folds a session before it hurts.
API & SDK2026-08-19
The listen Stream Kept Reopening Against My Serverless MCP Server
No errors anywhere, yet connections to the MCP server kept piling up overnight. Here is what happens when a fixed platform timeout meets a resubscribe loop, reproduced in a few dozen lines.
📚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