CLAUDE LABEN
TEACHERS — AnthropicがClaude for Teachersを公開。米国K-12の認証済み教員に、プレミアム機能と授業向けスキル、全50州の学習指導基準に沿ったカリキュラム連携を無償提供しますADMIN — Claude Enterprise全組織でAdmin APIがベータ提供に。メンバーと招待の操作はベータヘッダー不要、グループとカスタムロールは専用ヘッダーが必要ですM365 — Microsoft 365コネクタに書き込みツールが追加。メールの下書き・送信・整理、カレンダー操作、OneDriveとSharePointのファイル作成・更新まで任せられますMCP — --mcp-configや.mcp.jsonのサーバ単位request_timeout_msが無視され、新規セッションで60秒既定のままタイムアウトする不具合が修正されましたSUBAGENT — --forward-subagent-textとCLAUDE_CODE_FORWARD_SUBAGENT_TEXTが追加。サブエージェントのテキストと思考をstream-json出力に含められますDEADLINE — 7月24日にOpus 4.7のfastモードが削除されます。残り7日、speed: "fast" はエラーになるためOpus 4.8への移行をご確認くださいTEACHERS — AnthropicがClaude for Teachersを公開。米国K-12の認証済み教員に、プレミアム機能と授業向けスキル、全50州の学習指導基準に沿ったカリキュラム連携を無償提供しますADMIN — Claude Enterprise全組織でAdmin APIがベータ提供に。メンバーと招待の操作はベータヘッダー不要、グループとカスタムロールは専用ヘッダーが必要ですM365 — Microsoft 365コネクタに書き込みツールが追加。メールの下書き・送信・整理、カレンダー操作、OneDriveとSharePointのファイル作成・更新まで任せられますMCP — --mcp-configや.mcp.jsonのサーバ単位request_timeout_msが無視され、新規セッションで60秒既定のままタイムアウトする不具合が修正されましたSUBAGENT — --forward-subagent-textとCLAUDE_CODE_FORWARD_SUBAGENT_TEXTが追加。サブエージェントのテキストと思考をstream-json出力に含められますDEADLINE — 7月24日にOpus 4.7のfastモードが削除されます。残り7日、speed: "fast" はエラーになるためOpus 4.8への移行をご確認ください
記事一覧/API & SDK
API & SDK/2026-04-20上級

Claude API のストリーミングで詰まる3つの落とし穴——実装してわかった本当の難しさ

Claude APIのストリーミング実装で実際にはまりがちなエラーハンドリング・バックプレッシャー・トークンカウントの3点を、動くコードと一緒に解説します。

Claude API115ストリーミング9TypeScript24エラーハンドリング6Anthropic SDK4

Claude APIのストリーミングを実装してすぐ、ReadableStream is not async iterable というエラーに2時間悩みました。公式のサンプルコードは動くのに、自分の Next.js プロジェクトに組み込んだ途端に動かない——そういう経験をされた方は多いのではないでしょうか。

この記事は、実際にいくつかのプロダクトで Claude API のストリーミングを実装してきた中で、ドキュメントだけでは気づきにくかったポイントを整理したものです。「なぜ動かないのか」という視点で書いていますので、すでに基本的な実装ができている方にとっても役立てていただけると思います。

落とし穴1: ランタイム環境の違いが原因のストリーム非互換

最初の問題は、Node.js と Edge Runtime(Vercel Edge Functions や Cloudflare Workers)でストリームの扱いが異なることです。

Anthropic SDK の stream() メソッドが返すのは、SDK 独自のラッパーオブジェクトです。これは AsyncIterable として使えますが、Web 標準の ReadableStream とは別物です。Next.js の App Router では API Route がデフォルトで Edge Runtime 上で動作することがあり、そこで for await...of を使おうとすると型不一致のエラーが出ます。

// ❌ Edge Runtime で詰まりやすいパターン
import Anthropic from '@anthropic-ai/sdk';
 
export const runtime = 'edge'; // ← これが曲者
 
const client = new Anthropic();
 
export async function POST(req: Request) {
  const stream = await client.messages.stream({
    model: 'claude-opus-4-6',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'Hello' }],
  });
 
  // Edge Runtime では for await が期待通りに動かないことがある
  for await (const chunk of stream) {
    // ...
  }
}

解決策は2つあります。Node.js Runtime に明示的に切り替えるか、Web 標準の ReadableStream を自前で構築するかです。

// ✅ Node.js Runtime で動く安定パターン
export const runtime = 'nodejs'; // Edge→Node.js に変更
 
export async function POST(req: Request) {
  const client = new Anthropic();
 
  const stream = client.messages.stream({
    model: 'claude-opus-4-6',
    max_tokens: 2048,
    messages: [{ role: 'user', content: await req.text() }],
  });
 
  // SDK の toReadableStream() で Web 標準に変換
  return new Response(stream.toReadableStream(), {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

私が最初にはまったのは、runtime = 'edge' の宣言を消し忘れたまま本番デプロイして、ローカルでは動くのに本番だけ壊れるという状態でした。ローカルの Next.js 開発サーバーは Node.js で動くので再現しないんです。

落とし穴2: エラーハンドリングのタイミング問題

ストリーミングでは、エラーが「ストリーム開始前」に起きるか「ストリーム中」に起きるかで対処が変わります。この2種類を同じ try-catch でまとめて処理しようとすると、片方が漏れます。

// ❌ エラーが漏れがちなパターン
async function streamMessage(content: string) {
  try {
    const stream = client.messages.stream({ ... });
 
    for await (const event of stream) {
      // ここの処理中にエラーが起きた場合...
      process.stdout.write(event.delta?.text ?? '');
    }
  } catch (e) {
    // 接続前のエラーは捕捉できるが、
    // ストリーム途中のネットワーク断は別の扱いが必要
    console.error(e);
  }
}

SDK が提供する stream.on('error', ...)stream.finalMessage() を組み合わせるのが実践的な対処法です。

// ✅ 2種類のエラーを分けて処理するパターン
async function streamMessage(content: string): Promise<string> {
  const stream = client.messages.stream({
    model: 'claude-opus-4-6',
    max_tokens: 2048,
    messages: [{ role: 'user', content }],
  });
 
  // ストリーム中のエラーを個別にハンドリング
  stream.on('error', (error) => {
    console.error('Stream error:', error);
    // 必要に応じてリトライロジックを追加
  });
 
  // テキストだけ逐次処理
  stream.on('text', (text) => {
    process.stdout.write(text);
  });
 
  // 完了を待って最終メッセージを取得
  // ここで接続前エラーも含めて throw される
  try {
    const finalMessage = await stream.finalMessage();
    return finalMessage.content[0].type === 'text'
      ? finalMessage.content[0].text
      : '';
  } catch (e) {
    // API エラー・タイムアウト等
    throw new Error(`Stream failed: ${e instanceof Error ? e.message : String(e)}`);
  }
}

finalMessage() は「ストリームが正常に完了したことを確認してから最終結果を返す」メソッドなので、部分的な応答しか受け取れていない状態で処理が続かないよう防ぐことができます。

落とし穴3: トークン数の見積もりミスによるコスト超過

ストリーミングを実装すると、「レスポンスが来ている」という感覚が強くなって、実際のトークン消費量が見えにくくなります。気づいたら1リクエストあたり 5,000 トークンを超えていた、ということが起きやすいです。

stream.finalMessage() の返り値には usage フィールドが含まれていて、実際の入力・出力トークン数が確認できます。

const finalMessage = await stream.finalMessage();
 
console.log({
  inputTokens: finalMessage.usage.input_tokens,
  outputTokens: finalMessage.usage.output_tokens,
  totalTokens: finalMessage.usage.input_tokens + finalMessage.usage.output_tokens,
  model: finalMessage.model,
});

私が実際に遭遇したのは、会話履歴をそのまま全件 messages に渡し続けていて、30往復後には入力トークンが 15,000 を超えていたケースです。会話の要約や、古いメッセージの削除ロジックは最初から設計に組み込んでおくべきでした。

// ストリーミングでコスト上限を設ける実践例
const MAX_INPUT_TOKENS = 8000;
 
function trimMessages(
  messages: Anthropic.MessageParam[],
  maxTokens: number
): Anthropic.MessageParam[] {
  // 概算:1メッセージ平均500トークンと仮定して直近N件を保持
  const estimatedPerMessage = 500;
  const maxMessages = Math.floor(maxTokens / estimatedPerMessage);
 
  if (messages.length <= maxMessages) return messages;
 
  // 最初のシステムコンテキスト(user メッセージ)は保持
  const systemMessage = messages[0];
  const recentMessages = messages.slice(-(maxMessages - 1));
  return [systemMessage, ...recentMessages];
}

正確なトークン数が必要な場合は client.messages.countTokens() を使えますが、ストリーム送信前に毎回呼ぶと余分なAPIコールが発生します。バランスとしては、会話が一定件数を超えたらトリムする閾値ベースのアプローチが現実的だと思っています。

ストリーミングの本質的な難しさ

振り返ると、ストリーミングが難しいのは「見た目は動いている」ことが多いからだと感じています。エラーが出ていなくて、テキストは流れてくる。でも、エラーハンドリングが不完全だったり、トークンが想定以上に消費されていたりします。

本番環境でストリーミングを安定させるには、「動く」ではなく「壊れたときに何が起きるか」を最初から設計しておくことが大切です。接続が切れたときのリトライ、トークン上限への対策、ログ収集——これらを後から追加しようとすると、アーキテクチャの変更を迫られることがあります。

Claude API のストリーミングは、きちんと実装すればユーザー体験を大きく向上させる機能です。ただ、その「きちんと」の中身は、ドキュメントには書かれていない部分が結構あります。この記事が、その「書かれていない部分」を補う一助になれば嬉しいです。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-05-04
Bun × Claude API で本番運用するエッジAIサービス — Node.js移行の判断軸と落とし穴を回避する実装パターン
Claude APIをBun上で本番運用するための実装ガイド。Node.jsからの移行判断軸・組み込みSQLite/WebSocketの活用・ストリーミング最適化・典型的な落とし穴の回避策まで、動作するコードと実測値で解説します。
API & SDK2026-04-13
Claude API のSSEストリーミングをNext.js App Routerに実装する
Claude APIのServer-Sent EventsストリーミングをNext.js App Routerに実装する方法を解説。ReadableStream・React Hooks・エラーハンドリングまで、実際に動くコードで順を追って説明します。
API & SDK2026-07-09
RAG が自信たっぷりに間違え始めたとき — 検索の取りこぼしを接地率で計測する運用メモ
Claude API の RAG で、回答は流暢なのに事実がずれ始める。多くの場合、原因は検索側で答えを含む文書を取りこぼすサイレントなリコール低下です。接地率と検索ヒット率を計測し、段階的に立て直した運用メモを、動くコードと実数値付きで残します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →