◉CLAUDE LABEN
●2.1.312 — 巨大な CLAUDE.md の上書きや、cd のあとにスキルが見つからない問題を修正しました●API — Messages API の最大リクエストサイズが 32MB から 128MB に拡大されました(10/28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り31日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●API — 新しい API バージョン 2026-10-21 では temperature・top_p・top_k を渡すと 400 になります●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.312 — 巨大な CLAUDE.md の上書きや、cd のあとにスキルが見つからない問題を修正しました●API — Messages API の最大リクエストサイズが 32MB から 128MB に拡大されました(10/28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り31日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●API — 新しい API バージョン 2026-10-21 では temperature・top_p・top_k を渡すと 400 になります●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/API & SDK
⬡ API & SDK/2026-07-31上級

400文字だけ残したはずのツール出力が、1MB を握り続けていた

自作の MCP サーバーやツールラッパーで出力を切り詰めても、Node の sliced string は元の全文を握り続けます。境界が13文字であること、平坦化が効く書き方と効かない書き方、ヒープスナップショットから親を数える監査スクリプトまで、すべて実測して記録しました。

MCP59Node.js4メモリ9パフォーマンス4運用設計30

✦ プレミアム記事

自分のサイトのビルドログを読ませるだけの、ごく小さな MCP サーバーを書きました。ツールはひとつ、指定したログファイルを読んで返すだけです。返り値が大きくなりすぎないよう、先頭400文字だけをプレビューとして履歴に残し、全文は必要になったときに読み直す設計にしました。

個人開発でいくつものサイトを回していると、こうした小さな常駐プロセスが少しずつ増えていきます。 一つひとつは数十メガバイトのつもりでした。

数時間走らせたあと、ps の出力を見て手が止まりました。常駐プロセスの RSS が 200MB を超えています。

履歴に積んであるのは400文字のプレビューだけのはずです。何百件積んでも数十キロバイトにしかなりません。それでもメモリは減りませんでした。

原因にたどり着くまで、私はまったく違う場所を疑っていました。ストリームの読み残し、Buffer の解放漏れ、イベントリスナの積み上がり。そのどれでもありませんでした。

犯人は、切り詰めるために書いたたった一行の slice でした。

400文字のプレビューが、親の全文を握って離さない

まず最小の再現から始めます。4MB の文字列を20回作り、それぞれ先頭200文字だけを配列に残します。それだけのコードです。

// exp1.mjs — node --expose-gc exp1.mjs slice
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;  // 4MB のツール出力を模擬
  kept.push(full.slice(0, KEEP));            // 200文字だけ残すつもり
}
 
global.gc();
const after = process.memoryUsage().heapUsed;
 
console.log('kept =', kept.length,
            '/ previewLen =', kept[0].length,
            '/ heapDelta =', mb(after - base), 'MB');

global.gc() を明示的に呼んでいるのは、GC のタイミングに結果が左右されないようにするためです。--expose-gc フラグが必要になります。

実行結果です。計測環境は Node.js v22.22.3(V8 12.4.254.21)、Linux x86_64・4 vCPU・メモリ 3.9GB のサンドボックスです。

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

保持しているのは200文字の文字列が20本、合計4,000文字です。それなのに 80MB が回収されていません。4MB × 20 と、きれいに一致します。

つまり、切り詰めたはずの親文字列が20本とも生きています。

境界はちょうど13文字だった

ここで気になったのは、「では何文字までなら安全なのか」という点です。プレビュー長だけを変えて測り直しました。

// exp3.mjs — node --expose-gc exp3.mjs <KEEP>
const KEEP = Number(process.argv[2]);
const N = 10;
const SIZE = 2 * 1024 * 1024;
const kept = [];
 
global.gc();
const base = process.memoryUsage().heapUsed;
for (let i = 0; i < N; i++) {
  const full = 'x'.repeat(SIZE) + '#' + i;
  kept.push(full.slice(0, KEEP));
}
global.gc();
console.log('keep =', KEEP, '/ retained =', ((process.memoryUsage().heapUsed - base) / 1048576).toFixed(2), 'MB');

2MB × 10本 を作った場合の結果です。

プレビュー長(文字)回収されずに残った量解釈
12.00 MB親は解放されている
52.00 MB親は解放されている
112.00 MB親は解放されている
122.00 MBここまでは安全
1320.00 MBここから親を握る
1420.00 MB親を握る
20020.00 MB親を握る

12と13のあいだで、2MB から 20MB へ跳ねます。

残り続ける 2.00 MB は、ループの最後に作った full がまだ到達可能なままである分です。呼び出し回数を1回にしても40回にしても 2.00 MB のまま動かなかったので、こちらは定数のノイズだと判断しました。逆に13文字以上では、回数に正比例して積み上がります。

呼び出し回数残った量(プレビュー200文字・親4MB)
14.00 MB
28.00 MB
520.00 MB
1040.00 MB
2080.00 MB
40160.00 MB

きれいな直線です。長く走らせるほど確実に増えます。

この13という値は、V8 が部分文字列を「親への参照+開始位置+長さ」で表現する内部表現(sliced string)を使い始める最小長と一致します。短い切り出しはその場でコピーされるので親が残らない、というわけです。

短く切るほど安全、という感覚とは逆になります。プレビューを 12 文字に縮めれば回避できますが、12文字のプレビューには何の情報も載りません。実務では使えない回避策です。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦プレビュー長13文字を境に親文字列がまるごと生き残る挙動の実測(20回の呼び出しで 4MB → 80MB)
✦平坦化が効く5つの書き方と、効かない7つの書き方の比較表 — テンプレートリテラル・String()・repeat(1) はいずれも素通り
✦常駐ツールラッパーの200呼び出しで heapUsed 203.5MB → 4.6MB・RSS 245.0MB → 43.6MB に落ちた実測と、その代償の 16 ミリ秒
✦ヒープスナップショットから sliced string の親を集計し、予算超過で exit 1 を返す約50行の監査スクリプト(実行結果つき)
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ API & SDK2026-07-24
共通エージェント定義をセッション単位で使い分ける運用設計
Managed Agents で土台のエージェント定義を1つに畳み、バージョン固定とセッションローカルな上書きを使い分ける設計を、検証付きのコードと運用の落とし穴とともにまとめました。
⬡ API & SDK2026-09-21
非推奨の通知が出た朝、設定を書き換える前にバージョンを確かめた話
Claude Desktop / Cowork の管理構成キーが 10 月 7 日に受付終了します。先に書き換えると壊れるもの、触らなくても意味が変わるものを仕分ける手順を書き残します。
⬡ API & SDK2026-08-30
モデルIDを一括置換する前に、手元の CLI が抱えるカタログを引きます
新しい世代が出ると、前の世代のモデルIDが急に古く見えます。インストール済みの実行ファイルに埋め込まれたカタログを引き、一致・日付不一致・書式破損の三段に仕分けてから直す手順を、実測値とスクリプトつきでまとめました。