一言でいうと: 画像データは完全に取得済みで、問題なく画像としてデコードできます。止まっているのは HTTP 転送のいちばん最後のステップ、つまりクライアントに「これで全部です」と伝える部分です。したがって、正しい修正はタイムアウトを長くすることでも再試行することでもなく、データが届いたら自分でレスポンスを完了させ、すでに受け取っている画像を使うことです。
症状
ネイティブの画像生成エンドポイント(POST /v1beta/models/{model}:generateContent)を呼び出すと、次の組み合わせが見られることがあります。
- ダッシュボードのログではリクエストが 成功 し、課金済み になっている;
- それでもクライアントはハングし、自身の読み取りタイムアウトが発火するまで失敗しない;
- エラーは
Read timed out、ETIMEDOUT、またはUND_ERR_BODY_TIMEOUTのように見える。
ウィンドウ単位で発生します
これが重要なのは、再現方法と見え方が変わるからです。- ウィンドウ内では: 連続した呼び出しは、例外なくすべてハングします;
- ウィンドウ外では: 何十回連続で呼び出してもまったく問題なく動き、1 回も発生しません。
ストリーミング リクエスト(
:streamGenerateContent)とテキストのみのモデルは、一般的に影響を受けません。このページでは、非ストリーミングの画像生成 について説明します。応答ボディは大きく、2K 画像の JSON ボディはおよそ 13 MB になります。原因
画像レスポンスはTransfer-Encoding: chunked で送信されます。HTTP/1.1 の仕様では、サーバーが最後のデータチャンクを送信したら、クライアントに「これで終了です」と知らせるために 終端チャンク(ゼロ長のチャンク)を送る必要があります。
失敗しているのはその手順です: すべてのデータチャンクは届きますが、終端チャンクは送信されず、接続も閉じられません。
クライアントには、本文が終了したことを知る方法がないまま、完全でそのまま使える JSON ドキュメント(画像は base64 デコードできます)だけが残ります。そのため、クライアントは待ち続けます — 自身の読み取りタイムアウトが発火するまで。
たとえば、荷物はすでに玄関先に届いているのに、配達員が「配達完了」を押し忘れたようなものです。荷物はすぐ外にあるのに、あなたは追跡ページの更新を待ち続けています。
対処方法を直接左右する 3 つの結論は次のとおりです:
データは完全です
パケット損失でも、ネットワーク品質の問題でも、途中で転送が切れたわけでもありません。手元にあるバイト列はきれいにパースでき、画像は完全に利用可能です。
待っても意味はありません
一度ハングすると、サーバーは 1 バイトも 追加で送りません。330 秒 待っても変化がないことを確認済みです。タイムアウトを数百秒に延ばしても、検知が遅れるだけです。
1 台のマシンに固定されていません
ある時間帯の内部では、複数のプレゼンス拠点が 同時に 失敗し、同時に 回復します。ドメインや入口を切り替えても回避できません — クライアント側で処理する必要があります。
それを見分ける方法
次の 3 つがすべて当てはまるなら、ほぼ確実にこれに該当します:1
レスポンスに Transfer-Encoding: chunked があり、Content-Length がありません
つまり、本文の長さが事前に宣言されていないため、クライアントは終了チャンクだけを頼りに完了を判断するしかありません。
2
ここまでに受信した bytes は、すでに完全な JSON として parse できます
json.loadsを今ある内容に対して実行すると成功し、base64 の中の inlineData.data は完全でそのまま使える画像として base64 デコードされます。3
その parse に成功したあと、新しい bytes が長時間届きません
終了チャンクもなく、接続も閉じられません。ただ開いたままの状態が続きます。
2 つの似たケースとの違い
3 つとも似たようなエラーに見えますが、根本原因も修正方法もまったく異なります。1 つの判定基準をすべてに当てはめないでください:
エラーが
ECONNRESET なら、それは別種の問題です。Connection Drops を参照してください。
互換レイヤー: クライアント側でレスポンスを完了する
考え方はシンプルです: 接続が終了するのを待たず、手元にある bytes が完全な JSON として解析できた時点でただちに完了します。重要: グレース期間を維持する
解析に成功した瞬間に終了しないでください。通常は、終了チャンクはすぐ次の TCP セグメントにあり、数ミリ秒しか離れていません。解析に成功した時点で打ち切ってしまうと、「終端が数ミリ秒遅れて届いた」だけなのに「サーバーが送ってこなかった」と誤判定してしまいます。 正しい方法は、いったん解析に成功したら、もう少しだけ待つことです(3〜5 秒をデフォルトにするとよいでしょう)。その待機中にバイトが届いたら、通常どおり続行します。何も届かなかった場合にだけ、スタックしたと判断して自分でレスポンスを終了します。応答を返す前に確認する項目
安価なものから高価なものへ順に並べています。どれか 1 つでも失敗したら、待機を続けてください。応答を完了してはいけません。
チェック 6 と 7 を組み合わせることで、偽陽性率はほぼゼロになります。応答は 1 つの JSON オブジェクトであるため、データがまだ欠けている間は必ずパースに失敗します。つまり、本当に完全に届いた応答だけを完了できます。
Python
ストリームをバックグラウンドスレッドで読み取り、メインスレッドで キューのタイムアウト を使って猶予時間を実装します:この版では、各 chunk のたびではなく、猶予時間が切れたときに 1 回だけ パースします — そのため、上のチェック 5 を自動的に満たせます。十数 MB の本文が何度もパースされることはありません。
Node.js
Node.js には追加のスレッドは不要です —reader.read() はすでに promise なので、Promise.race で次の chunk をどれだけ待つかを制限できます:
タイムアウトの選び方
ここで最も起こりやすいミスは、2つの異なる用途に1つのタイムアウト値を使うことです。「アップストリームが画像を生成するのを待つ時間」と、「最初のバイトの後に2つのチャンクの間で発生する無音」です。通常の所要時間は桁違いに異なります。これらを1つの値にまとめると、遅い生成を失敗のように切ってしまうか、実際に停止したリクエストを数分待たせることになります。✅ 推奨
2つを別々に設定してください。最初のバイトの前には生成のための余裕を持たせ、
その後はチャンク間の無音を数秒に抑え、上のクライアント側の完了処理で
残りを拾わせます。失敗は数秒で表面化し、正常なリクエストは
影響を受けません。
❌ 避ける
すべてを「念のため」でまとめた300秒のタイムアウトを1つだけ使うこと。
レスポンスが固まると、サーバーはそれ以上何も送らないので、待ち時間を伸ばしても
何も変わらず、検知が遅れるだけです。
4Kのような遅いティアを含む製品の場合: 総タイムアウトは長くして構いません(モデルには本当にその時間が必要です)が、チャンク間の無音しきい値はそれに合わせて長くすべきではありません。これらは別のものなので、一緒にスケールしないでください。
Node.js ユーザー向け: 組み込みの
fetch の背後にあるエンジンである undici は、Node 18+ では3つの独立したタイムアウトを持っており、SDK の timeout オプションはそれらのどれにも影響しません。正しい設定については、接続の切断の「Node.js: 3つの独立したタイムアウト」セクションを参照してください。再試行と課金
サーバーが応答を最後まで返し終えていないと判断できたら、次の順序で確認してください。1
既にある画像を使ってください — ほとんどの場合、これで終わります
データは完全で、画像はそのまま利用できるため、再試行は不要です。これが最も安価な方法であり、二重に課金されるのを避けられます。
2
解析が本当に失敗した場合のみ再試行してください
手元のバイト列を完全な JSON に解析できない場合にのみ、再試行してください。新しい接続を使い、試行の間隔は2〜3秒空けてください。
3
連続失敗したら、詰めて再試行せずに間を空けてください
問題はウィンドウ単位で発生するため、すぐに再試行すると同じウィンドウに入る可能性が高くなります。3回連続でハングしたら、30秒待ってから再度試してください。
ロールアウトノート
これらは言語やフレームワークに依存せず、私たち自身のロールアウトから得たものです。- 単一のネットワーク層エントリポイントに置き、呼び出し箇所に散らさないでください。 それを「リクエストを送信する」アクションそのものの一部にします。そうすれば、すべての画像パスを一度にカバーでき、ビジネスコードには手を加えずに済み、サーバー側が修正されたあとに変更が必要なのも1か所だけになります。
- 実際に必要なのは段階的な読み取りアクセスです。 前提条件は、レスポンスが終了する前に到着した内容を確認できることです。ほとんどすべての HTTP クライアントはこれを提供しています(ストリーミング読み取り、チャンクコールバック、進捗イベント)が、通常はデフォルトではありません — デフォルトの「ボディ全体をそのまま返してほしい」は、まさにハングする経路です。ここが作業の大半を占めます。
- 最後のチャンクを受信してからの時間で判断し、リクエスト開始時点からは数えないでください。 チャンクのたびに猶予時間タイマーをリセットします。そうすれば、遅いネットワークを不当に罰することもなく、何も動いていない状態も見逃しません。
- スイッチの背後に隠してください。 いつでもオフにできるフラグの背後にこの挙動を置いておきます。リリース直後に予期しないことが起きても、オフに切り替えれば以前の挙動に戻せます。緊急デプロイは不要です。
- テレメトリを追加してください。 完了パスが発火するたびにログを記録します(タイムスタンプ、バイト数、待機時間)。これには3つの目的があります。実際にどの程度発生しているかを定量化すること、レイヤーが期待どおり機能していることを確認すること、そしてサーバー側の修正後にカウンターがゼロまで下がることを確認することです。これが、そのレイヤーを廃止できると判断する唯一の客観的根拠です。
- その作業中にできるおまけの改善があります。 段階的な読み取りができるようになれば、ユーザーに実際の進捗を表示できます(「データを受信中、X.X MB」)。それまでの長いダウンロードは、ユーザー側からは完全なブラックボックスでした。
どのように自分たちでデプロイしたか
この作業はすでに自社の AI 画像スタジオ(imagen.apiyi.com)で完了し、検証済みです。障害を再現するモックサービス(body 全体を送信したあと、終了を通知せず接続も閉じない)を使って、次の比較を行いました。
結論: 正常なリクエストにはゼロ影響で、失敗するリクエストは「タイムアウトを待ってから失敗」から「数秒以内に画像を取得」へ変わります。
よくある質問
これは実際には正常なリクエストを途中で打ち切ってしまうことがありますか?
これは実際には正常なリクエストを途中で打ち切ってしまうことがありますか?
いいえ。完了とみなすには、受信した bytes が 完全な JSON ドキュメント としてパースできる必要があります。データがまだ不足している間は、必ずパースに失敗します。さらに 3〜5 秒の猶予期間を上乗せするため、正常なリクエストが誤判定されることはありません。上の比較表の最初の 2 行は、まさにこの 2 つのケースを並べて示しています。
途中で画像が半分だけ届くことはありますか?
途中で画像が半分だけ届くことはありますか?
いいえ。確認しているのは画像そのものではなく、レスポンスボディ全体 の整合性です。JSON がパースできれば、画像データは完全です。画像が半分しかない場合はパース失敗に相当し、その場合に完了扱いになることはありません。
これはサーバー側の問題を取り繕っているだけではありませんか?
これはサーバー側の問題を取り繕っているだけではありませんか?
いいえ。これはサーバー側の修正の代わりではありません。すでに生成され、すでに課金済みの結果をユーザーの手元に届けつつ、無条件のリトライが引き起こす二重課金を避けます。また、ここで得られるテレメトリは、障害がいつ発生するかの把握にも役立ちます。
サーバー側が修正されたら、これは削除すべきですか?
サーバー側が修正されたら、これは削除すべきですか?
急ぐ必要はありません。終了シグナルがある場合、このロジックは発火しないため、コストはかかりません。テレメトリが長期間ゼロのままになってから、整理を検討してください。
サポートに連絡するタイミング
上記の互換性レイヤーを追加したあとでも、次のいずれかが当てはまる場合は、資料をそろえてサポートに連絡してください。- お手元の bytes が 最後までそろっても完全な JSON として parse されない(これはこのページのシナリオではありません。転送が実際に途中で切れています);
- クライアント側で completion しても、長時間 response headers がまったく返ってこない(つまり、上流はまだ送信を開始していません。遅い生成、または上流側の障害であり、completion の問題ではありません);
- 停止率が特定の時間帯に集中するのではなく、一貫して高いままで、長期間にわたって安定して再現する。
x-request-id、呼び出し時刻(timezone 付き、例: 2026-08-03 13:15 (UTC+8))、model 名と imageSize のような主要パラメータ、生の client-side exception、そして停止した時点で受信していた bytes 数を含めてください。
関連ドキュメント
接続の切断
ECONNRESET、SSL EOF、undici の 3 つのタイムアウト、ローカルプロキシの診断マトリクス必読 & ベストプラクティス
同期呼び出し、タイムアウト階層、base64 の扱い、および切断された接続に対する課金
独自の非同期キューを構築する
同期呼び出しをタスクキューでラップし、再試行と永続化でまれな失敗を吸収する