GEMINI LABEN
NANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定ですNANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定です
記事一覧/API / SDK
API / SDK/2026-03-29中級

Geminiストリーミング応答が途切れる時の対処法 — 原因特定から再接続まで

Gemini API のストリーミング応答が途中で切れてしまう問題を解決。ネットワークタイムアウト、チャンク処理エラー、トークン上限超過、安全フィルター中断など、原因別の対処方法と再接続ロジックを実装例付きで解説。

gemini-api279streaming18troubleshooting57connection

ストリーミングで文字が流れている途中、ふつりと止まる。例外は飛ばず、ユーザーの画面には書きかけの文章だけが残ります。この静かな失敗が、いちばん厄介です。

原因はネットワークだけではありません。厳しすぎるタイムアウト、チャンク処理の取りこぼし、トークン上限への到達、安全フィルターの発動。切り分け方も直し方もそれぞれ違うので、順に潰していきます。

ストリーミング応答が途切れる5つの原因

原因を特定するチェックリスト

  1. ネットワークタイムアウト — クライアント側またはサーバー側の接続不安定
  2. チャンク処理エラー — JSON パースに失敗した場合
  3. トークン上限超過 — リクエスト+応答のトークン数が上限を超えた
  4. 安全フィルター発動 — Gemini が有害なコンテンツを検出して応答中止
  5. バックプレッシャー — クライアント側の処理が追いつかず、接続がタイムアウト

それぞれの原因と対処法を見ていきましょう。

原因1:ネットワークタイムアウト

タイムアウト発生時のエラーメッセージ

TimeoutError: The operation timed out
socket.timeout: _ssl.c:997: The handshake operation timed out

このエラーは、サーバーからの応答がクライアントに到着する前に、接続がタイムアウトしたことを示します。

タイムアウト時間の設定

Python の requests ライブラリでは、デフォルトのタイムアウトが非常に短く設定されています。ストリーミング応答用に明示的に長めのタイムアウトを設定する必要があります。

import requests
import json
from typing import Generator
 
def stream_gemini_with_timeout(prompt: str, timeout: int = 60) -> Generator[str, None, None]:
    """
    ストリーミングレスポンスを取得し、テキストを逐次利用
 
    Args:
        prompt: ユーザープロンプト
        timeout: タイムアウト秒数(デフォルト60秒)
    """
    url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent"
    headers = {
        "x-goog-api-key": "YOUR_GEMINI_API_KEY",
        "Content-Type": "application/json"
    }
 
    payload = {
        "contents": [
            {
                "role": "user",
                "parts": [{"text": prompt}]
            }
        ]
    }
 
    try:
        response = requests.post(
            url,
            json=payload,
            headers=headers,
            stream=True,
            timeout=timeout  # ストリーミング用に十分な時間を設定
        )
        response.raise_for_status()
 
        for line in response.iter_lines():
            if line:
                try:
                    data = json.loads(line)
                    # テキストを抽出
                    if "candidates" in data and data["candidates"]:
                        candidate = data["candidates"][0]
                        if "content" in candidate:
                            for part in candidate["content"]["parts"]:
                                if "text" in part:
                                    yield part["text"]
                except json.JSONDecodeError:
                    print(f"Failed to parse line: {line}")
 
    except requests.exceptions.Timeout:
        print(f"Streaming timed out after {timeout} seconds")
        raise
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        raise
 
# 使用例
for chunk in stream_gemini_with_timeout("Tell me about machine learning"):
    print(chunk, end="", flush=True)

Server-Sent Events(SSE)での実装

より堅牢なストリーミング実装には、Server-Sent Events がお勧めです。

from aiohttp import ClientSession
import asyncio
 
async def stream_gemini_sse(prompt: str, timeout: int = 300):
    """
    aiohttp を使った非同期ストリーミング
    timeout デフォルト = 5分(長い応答対応)
    """
    async with ClientSession(timeout=timeout) as session:
        url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent"
        headers = {"x-goog-api-key": "YOUR_GEMINI_API_KEY"}
        payload = {
            "contents": [{"role": "user", "parts": [{"text": prompt}]}]
        }
 
        async with session.post(url, json=payload, headers=headers) as resp:
            async for line in resp.content:
                try:
                    data = json.loads(line)
                    if "candidates" in data:
                        for part in data["candidates"][0]["content"]["parts"]:
                            if "text" in part:
                                yield part["text"]
                except (json.JSONDecodeError, KeyError):
                    continue
 
# 使用例
async def main():
    async for chunk in stream_gemini_sse("Explain quantum computing"):
        print(chunk, end="", flush=True)
 
asyncio.run(main())

原因2:チャンク処理エラー

JSON パースエラーの検出と対処

ストリーミング応答は JSON 形式で返されますが、ネットワークの問題でデータが破損することがあります。

import json
import logging
 
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
 
def stream_with_error_handling(prompt: str) -> str:
    """
    チャンクエラーを検出して、安全にスキップ
    """
    accumulated_text = ""
    error_count = 0
    max_errors = 5  # 最大エラー許容数
 
    for line in stream_gemini_with_timeout(prompt):
        try:
            # 各チャンクが有効な JSON か確認
            if isinstance(line, str):
                accumulated_text += line
            else:
                raise ValueError(f"Unexpected chunk type: {type(line)}")
 
        except (json.JSONDecodeError, ValueError) as e:
            error_count += 1
            logger.warning(f"Chunk parse error #{error_count}: {e}")
 
            if error_count > max_errors:
                logger.error(f"Too many errors ({error_count}). Stopping stream.")
                break
            # エラーをスキップして続行
            continue
 
    return accumulated_text
 
# 使用例
result = stream_with_error_handling("What is artificial intelligence?")
print(result)

UTF-8 デコードエラーの回避

バイナリチャンクが不完全な状態で到着する場合があります。

def safe_decode_chunk(chunk: bytes) -> str:
    """
    不完全な UTF-8 シーケンスをスキップ
    """
    try:
        return chunk.decode('utf-8')
    except UnicodeDecodeError as e:
        # 不完全な UTF-8 シーケンスをスキップして再試行
        return chunk[:e.start].decode('utf-8', errors='ignore')

原因3:トークン上限超過による途中停止

token_count_metadata での確認

Gemini API の応答には、トークン使用量のメタデータが含まれています。これを監視することで、上限到達を事前に検知できます。

def stream_with_token_monitoring(prompt: str, max_tokens: int = 8192):
    """
    トークン上限を監視しながらストリーミング
    """
    total_tokens = 0
    accumulated_text = ""
 
    for chunk_data in stream_gemini_with_timeout(prompt):
        # メタデータからトークン数を取得
        if "usageMetadata" in chunk_data:
            usage = chunk_data["usageMetadata"]
            output_tokens = usage.get("outputTokens", 0)
            total_tokens += output_tokens
 
            print(f"Token usage: {total_tokens}/{max_tokens}")
 
            if total_tokens >= max_tokens:
                print("⚠️ Token limit approaching. Stopping stream.")
                break
 
        # テキストを追加
        if "candidates" in chunk_data:
            for part in chunk_data["candidates"][0]["content"]["parts"]:
                if "text" in part:
                    accumulated_text += part["text"]
 
    return accumulated_text, total_tokens
 
# 使用例
text, tokens = stream_with_token_monitoring("Write a long essay about AI", max_tokens=5000)
print(f"Generated {tokens} tokens")

max_output_tokens の事前指定

応答の途中で打ち切られるのを防ぐには、事前に十分な max_output_tokens を設定します。

def create_stream_with_sufficient_tokens(prompt: str, estimated_output_tokens: int = 2000):
    """
    十分なトークン枠を確保してストリーミング開始
    """
    api_params = {
        "model": "gemini-2.0-flash",
        "contents": [{"role": "user", "parts": [{"text": prompt}]}],
        "generation_config": {
            "max_output_tokens": estimated_output_tokens,  # 余裕を持たせる
            "temperature": 0.7
        }
    }
 
    # API 呼び出し処理
    # ...ストリーミング実装...
    pass

原因4:安全フィルター発動による中断

SAFETY_REASON エラーの検出

Gemini は有害なコンテンツを検出すると、応答を中止する場合があります。

def stream_with_safety_detection(prompt: str) -> dict:
    """
    安全フィルター状態を監視
    """
    result = {
        "text": "",
        "finish_reason": None,
        "safety_ratings": []
    }
 
    for chunk in stream_gemini_with_timeout(prompt):
        # finish_reason を確認
        if "candidates" in chunk:
            candidate = chunk["candidates"][0]
 
            # 終了理由を確認
            if "finishReason" in candidate:
                result["finish_reason"] = candidate["finishReason"]
 
                if candidate["finishReason"] == "SAFETY":
                    print("⚠️ Response blocked by safety filter")
                    break
 
            # 安全評価を記録
            if "safetyRatings" in candidate:
                result["safety_ratings"] = candidate["safetyRatings"]
 
        # テキスト追加
        if "content" in chunk["candidates"][0]:
            for part in chunk["candidates"][0]["content"]["parts"]:
                if "text" in part:
                    result["text"] += part["text"]
 
    return result
 
# 使用例
output = stream_with_safety_detection("Some potentially sensitive prompt")
if output["finish_reason"] == "SAFETY":
    print("The request triggered the safety filter")
else:
    print(output["text"])

SafetySettings のカスタマイズ

安全設定を調整することで、フィルター強度を変更できます(※ユースケースに応じて)。

def stream_with_custom_safety_settings(prompt: str):
    """
    安全フィルター設定をカスタマイズ
    """
    safety_settings = [
        {
            "category": "HARM_CATEGORY_HARASSMENT",
            "threshold": "BLOCK_MEDIUM_AND_ABOVE"  # より多くのコンテンツを許可
        },
        {
            "category": "HARM_CATEGORY_HATE_SPEECH",
            "threshold": "BLOCK_ONLY_HIGH"
        }
    ]
 
    # API リクエストに safety_settings を含める
    # ...実装...
    pass

原因5:バックプレッシャーと処理速度の不一致

バックプレッシャー制御の実装

サーバーからの応答速度がクライアント処理速度を上回ると、バッファオーバーフロー が発生します。

import asyncio
from asyncio import Queue
 
class BackpressureControlledStream:
    def __init__(self, max_queue_size: int = 10):
        self.queue = Queue(maxsize=max_queue_size)
        self.max_queue_size = max_queue_size
 
    async def producer(self, prompt: str):
        """ストリーミング応答を キューに入れる"""
        async for chunk in stream_gemini_sse(prompt):
            await self.queue.put(chunk)
        await self.queue.put(None)  # 終了シグナル
 
    async def consumer(self):
        """キューから読み出して処理"""
        accumulated = ""
        while True:
            # キューサイズを監視
            queue_size = self.queue.qsize()
            if queue_size > self.max_queue_size * 0.8:
                print(f"⚠️ Backpressure: {queue_size}/{self.max_queue_size}")
 
            chunk = await asyncio.wait_for(self.queue.get(), timeout=30)
            if chunk is None:
                break
            accumulated += chunk
 
        return accumulated
 
    async def run(self, prompt: str) -> str:
        """プロデューサー&コンシューマーを並行実行"""
        producer_task = asyncio.create_task(self.producer(prompt))
        consumer_task = asyncio.create_task(self.consumer())
 
        result = await consumer_task
        await producer_task
        return result
 
# 使用例
async def main():
    stream = BackpressureControlledStream(max_queue_size=20)
    result = await stream.run("Explain neural networks")
    print(result)
 
asyncio.run(main())

再接続ロジックの実装

自動リトライ付き再接続戦略

ストリーミングが途切れた場合、自動的に再開する仕組みが重要です。

import time
from typing import Optional
 
class ResilientGeminiStream:
    def __init__(self, max_retries: int = 3, backoff_factor: float = 2.0):
        self.max_retries = max_retries
        self.backoff_factor = backoff_factor
 
    def stream_with_retry(self, prompt: str) -> str:
        """
        ストリーミングが途切れた場合、自動的に再接続
        """
        accumulated_text = ""
        attempt = 0
 
        while attempt < self.max_retries:
            try:
                for chunk in stream_gemini_with_timeout(prompt):
                    accumulated_text += chunk
                    # 成功時
                return accumulated_text
 
            except (TimeoutError, ConnectionError) as e:
                attempt += 1
                if attempt >= self.max_retries:
                    raise RuntimeError(f"Failed after {self.max_retries} retries: {e}")
 
                wait_time = self.backoff_factor ** attempt
                print(f"Reconnecting in {wait_time}s (attempt {attempt}/{self.max_retries})")
                time.sleep(wait_time)
 
        return accumulated_text
 
# 使用例
client = ResilientGeminiStream(max_retries=3)
try:
    result = client.stream_with_retry("Tell me about quantum physics")
    print(result)
except RuntimeError as e:
    print(f"Failed to get streaming response: {e}")

チェックポイント ベースの再開

長いストリーミング応答では、定期的にチェックポイントを保存すると、途中から再開できます。

import json
 
def stream_with_checkpoints(prompt: str, checkpoint_file: str = "stream_checkpoint.json"):
    """
    チェックポイントを保存しながらストリーミング
    """
    # チェックポイントから再開
    checkpoint = {}
    try:
        with open(checkpoint_file, 'r') as f:
            checkpoint = json.load(f)
    except FileNotFoundError:
        pass
 
    accumulated_text = checkpoint.get("text", "")
    chunks_processed = checkpoint.get("chunks", 0)
 
    try:
        for i, chunk in enumerate(stream_gemini_with_timeout(prompt)):
            if i < chunks_processed:
                continue  # スキップして再開位置から処理
 
            accumulated_text += chunk
            chunks_processed += 1
 
            # 100チャンクごとにチェックポイント保存
            if chunks_processed % 100 == 0:
                with open(checkpoint_file, 'w') as f:
                    json.dump({
                        "text": accumulated_text,
                        "chunks": chunks_processed,
                        "timestamp": time.time()
                    }, f)
 
    finally:
        # 完了後、チェックポイントを削除
        import os
        if os.path.exists(checkpoint_file):
            os.remove(checkpoint_file)
 
    return accumulated_text

まとめ

Gemini API のストリーミング応答が途切れるのは、単なる一時的な問題ではなく、複数の原因から生じています。

  1. ネットワークタイムアウト: 十分なタイムアウト時間を設定
  2. チャンク処理エラー: JSON パースエラーハンドリングを実装
  3. トークン上限: max_output_tokens を事前指定
  4. 安全フィルター: finish_reason で SAFETY を検出
  5. バックプレッシャー: キューイングで処理速度を制御

さらに詳しく学びたい方は、Gemini リアルタイムストリーミング高度パターンで、SSE・WebSocket 連携の実装パターンを解説しています。

また、エラーハンドリング&リトライ完全ガイドでは、ストリーミング以外のエラーハンドリング全般を学べます。

本番環境でのストリーミング構築について詳しく知りたい方は、以下をお勧めします。

シェア

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

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

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

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

関連記事

API / SDK2026-05-23
Gemini API のストリーミングが本番で「途中で切れる」とき、私が試している原因切り分けの順序
Gemini API のストリーミング応答が本番で途中で切れる症状を、ネットワーク・SDK・サーバー側のどこから疑うかの順序を、個人開発で実際に踏んだ事例ごとに整理しました。
API / SDK2026-05-20
Gemini API のストリーミングが本番だけまとめて返ってくる — Cloud Run・Vercel・Cloudflare のバッファリング対処
ローカル開発では一文字ずつ流れるのに本番デプロイ後だけ応答が一括で返ってくる — Cloud Run / Vercel / Cloudflare Workers / Nginx で発生するストリーミングバッファリング問題を、原因と対処パターン別に整理しました。
API / SDK2026-04-26
Gemini API のレスポンスで日本語が文字化けする時、最初に見るべき4つの場所
Gemini API で日本語レスポンスが文字化けする原因は、API側ではなくクライアント側の4つのレイヤーに潜んでいます。HTTPデコード、ストリーミングのチャンク境界、出力先の encoding、サロゲートペアの順にチェックする実践ガイドです。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →