GEMINI LABEN
DESKTOP — Gemini の Windows デスクトップ版が出ました。Windows 10 以降に対応し、Alt + Space でどの画面からでも呼び出せます0.59.0 — Gemini CLI の現在の安定版はこれです。v0.60 と v0.61 は nightly と preview だけで、安定版タグはまだ出ていません9/30 — gemini-omni-flash-preview の停止まで残り16日です。後継の gemini-omni-1.1-flash は video_config に resolution が加わっており、ID を差し替えるだけでは済まない場合があります429 — fileData に外部 URL の画像や PDF を渡すと、クォータを 0.03% しか使っていなくても 429 が返る報告が続いています。テキストだけなら通りますNEW — Drive の原稿を直して聞き直したら、Gemini が直す前の文を返してきました。取り込みの癖と、再添付を決める基準を書きましたSAFETY — 小説の翻訳で PROHIBITED_CONTENT が続くとき、閾値を下げれば通る話と、下げても通らない話があります。その境目を先に見分けたいところですDESKTOP — Gemini の Windows デスクトップ版が出ました。Windows 10 以降に対応し、Alt + Space でどの画面からでも呼び出せます0.59.0 — Gemini CLI の現在の安定版はこれです。v0.60 と v0.61 は nightly と preview だけで、安定版タグはまだ出ていません9/30 — gemini-omni-flash-preview の停止まで残り16日です。後継の gemini-omni-1.1-flash は video_config に resolution が加わっており、ID を差し替えるだけでは済まない場合があります429 — fileData に外部 URL の画像や PDF を渡すと、クォータを 0.03% しか使っていなくても 429 が返る報告が続いています。テキストだけなら通りますNEW — Drive の原稿を直して聞き直したら、Gemini が直す前の文を返してきました。取り込みの癖と、再添付を決める基準を書きましたSAFETY — 小説の翻訳で PROHIBITED_CONTENT が続くとき、閾値を下げれば通る話と、下げても通らない話があります。その境目を先に見分けたいところです
記事一覧/API / SDK
API / SDK/2026-06-12中級

Gemini API の大規模障害で夜間バッチを止めないために組んだ三層の防御

今週の Gemini 広域障害で夜間バッチが3時間分の処理を取りこぼした反省から、リトライ・モデルフォールバック・縮退運転の三層防御に組み替えた記録です。再試行してよい失敗の見分け方、冪等性の確保、復旧後の追い付き処理まで実装コード付きで残します。

Gemini API239障害対応2リトライフォールバック2夜間バッチ

今週、Gemini API の広範な障害で、毎晩動かしている集計バッチが3時間分の処理を取りこぼしました。見慣れない番号のエラーが立て続けに返り、リトライも全部同じ顔で落ちていく。朝にダッシュボードを見た瞬間、「これはこちら側では何もできない種類の障害」と分かりましたが、本当の問題はそこではありませんでした。復旧したあと、取りこぼした3時間分を安全に流し直す手段を、私は用意していなかったのです。

幸い実害は軽く済みました。ただ、個人開発のサービスは自分が寝ている間も動き続けます。この機会に夜間バッチの防御を三層構成に組み替えたので、設計の判断と実装をまとめて残しておきます。前提は Node.js と公式 SDK の構成ですが、考え方は他の言語でもそのまま通じるはずです。

失敗を4種類に分けるところから始める

障害対応のコードを書く前に、まず「失敗」を分類しました。すべてを同じリトライ処理に放り込むのが一番危険だからです。

  • 一過性の失敗: 429 のレート超過、503 や今回のような広域障害。時間を置けば直る見込みがあります
  • 恒久的な失敗: 400 系の入力不正や認証エラー。何度送っても直りません
  • ネットワーク層の失敗: タイムアウトや接続断。リクエストが届いたかどうか不明な点が厄介です
  • 品質の失敗: ステータスは 200 でも、出力が壊れていて後段で使えないケース

この分類で大事なのは、機械的に再試行してよいのは1番目と3番目だけという点です。400 を再送し続けるのはクォータの無駄遣いですし、品質の失敗をネットワークと同じ仕組みで再試行すると、同じ壊れた出力を高い確率でもう一度受け取るだけです。品質の失敗には、温度を下げる・プロンプトを変えるといった「条件を変えた再試行」が要ります。

第一層: 再試行してよい失敗だけをリトライする

分類が決まれば、リトライ処理は短く書けます。

import { GoogleGenAI } from "@google/genai";
 
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
 
type FailureKind = "transient" | "permanent" | "network";
 
function classifyError(err: unknown): FailureKind {
  const status = (err as { status?: number }).status;
  if (status === 429 || (status !== undefined && status >= 500)) return "transient";
  if (status !== undefined && status >= 400) return "permanent";
  return "network"; // fetch 失敗・タイムアウトなど status が取れないもの
}
 
async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 4): Promise<T> {
  let lastError: unknown;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err;
      if (classifyError(err) === "permanent") throw err; // 再送しても直らない
      const base = 1000 * 2 ** attempt;
      const jitter = Math.random() * base * 0.5;
      await new Promise((resolve) => setTimeout(resolve, base + jitter));
    }
  }
  throw lastError;
}

ポイントは2つあります。まず、ジッター(待ち時間の乱数)を省かないこと。障害復旧の瞬間に全クライアントが同時に再送すると、それ自体が二次障害の引き金になります。次に、試行回数は4回程度で潔く諦めること。広域障害は数十分から数時間続くため、その場で粘るより、後述のキューに積んで撤退するほうが全体としては早く片づきます。

第二層: モデルのフォールバックチェーン

リトライで救えないとき、次に試すのはモデルの切り替えです。私の場合は gemini-3.5-flash を主にして、失敗時は gemini-3.1-flash へ落とす二段構えにしています。

const MODEL_CHAIN = ["gemini-3.5-flash", "gemini-3.1-flash"];
 
async function generateWithFallback(
  prompt: string
): Promise<{ text: string; degraded: boolean }> {
  for (const [index, model] of MODEL_CHAIN.entries()) {
    try {
      const res = await withRetry(() =>
        ai.models.generateContent({ model, contents: prompt })
      );
      return { text: res.text ?? "", degraded: index > 0 };
    } catch {
      // チェーンの次のモデルへ
    }
  }
  throw new Error("all models in chain failed");
}

注意したいのは、フォールバック先の出力品質を平時に確認しておくことです。障害の真っ最中に「代替モデルの出力が後段の処理を通らない」と気づくのが最悪のパターンなので、私はこのチェーンを月に一度、わざと主モデルを外して通しています。degraded フラグを結果に残しておくと、あとから品質を点検する対象も絞れます。

ただ、正直に書いておくと、今回のような広域障害ではフォールバック先も一緒に落ちる時間帯がありました。同一基盤の障害に対して、同一ベンダー内の迂回は思ったほど効きません。設計時の想定より厳しい現実でしたが、だからこそ次の第三層が要ります。

第三層: 縮退運転 — 止めないことを優先する

最後の層は「AI なしでも壊れない」ことです。私のバッチで Gemini が担っているのは要約と分類で、それが無くても元データの表示は成立します。そこでチェーン全滅時は、前回実行時の結果をそのまま使い回し、画面には「最新の分析は一時的に更新を停止しています」と小さく出すだけにしました。

縮退の設計で効いたのは、「何が欠けても画面が成立するか」を機能ごとに書き出したことです。一覧にしてみると、絶対に止められない処理は思ったより少ないものでした。AI の出力は多くの場面で「あれば嬉しい付加情報」であって、土台ではありません。土台でない部分の障害でサービス全体を道連れにしない。当たり前のようでいて、書き出すまで私はこの線引きを曖昧にしたままでした。

障害をどう検知するか — アラートの閾値で迷った話

三層の防御を組んだあと、最後に残ったのが検知の問題でした。単発の失敗でアラートを鳴らすと、平時の散発的な 503 で通知が埋まり、本当の障害の日に通知を読まなくなります。私自身、この「アラート疲れ」で重要な通知を見落とした経験が一度あります。

落ち着いた先は、単発の失敗率ではなく「連続失敗数」と「5分窓の失敗率」の二段構えです。連続5件の失敗、または5分間で失敗率が半分を超えたときだけ通知する。この閾値にしてから、平時の誤報はなくなり、今回の障害では開始から数分で通知が届きました。

もうひとつの発見は、自前のエラー分類カウンタのほうが公式のステータスページより早いことです。ステータスページの更新には時間差があります。classifyError が transient を数え始めた時点で異常は確定しているので、検知は自分のメトリクスを一次情報にして、ステータスページは「裏取り」に使う運用へ変えました。

冪等性がないと再試行は事故になる

三層の防御より地味で、しかし今回いちばん反省したのがここです。私自身、リトライ処理は書き慣れたつもりでいましたが、書き込み側の冪等化を後回しにしていました。

タイムアウトした呼び出しは「届いていない」とは限りません。届いて、処理されて、応答だけが落ちた可能性があります。そのため、書き込みを伴う処理には冪等キーを付けました。バッチの各項目に「日付 + 項目ID」のキーを振り、結果テーブルへの書き込みを upsert に変える。これだけで「同じ項目を二度処理して二重に書き込む」事故は構造的に起きなくなります。リトライ機構を足すなら、その前に書き込みの冪等化を済ませる。順番としてはこちらが先だった、というのが今回の学びです。

復旧後の追い付き処理と、結局いちばん効いたもの

障害が明けたあとの再処理にも設計が要ります。今回の私の失敗は、処理に失敗した項目の記録が「エラーログの中」にしかなかったことでした。ログから対象を拾い直すのは、二度とやりたくない作業です。

組み替え後は、失敗した項目を pending_retry テーブルに積み、バッチの冒頭で「積み残しがあれば先に消化する」一手を入れました。これで復旧後は次の定時実行が自然に追い付き処理を兼ねます。専用の復旧スクリプトを書かずに済む構成にしておくと、障害のたびの手作業が消えます。

振り返ると、今回の障害で効いたのは高度な仕組みではなく、「失敗の分類」「冪等キー」「積み残しキュー」という地味な三点でした。夜間バッチを Gemini API に依存させているなら、次の障害が来る前に、失敗した項目がどこに記録されるかだけでも確認しておくと、復旧の朝がずいぶん楽になるはずです。

シェア

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

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

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

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

関連記事

API / SDK2026-09-12
10月2日に止まる gemini-2.5-flash-image の移行先は、公式表のままでは動きません
gemini-2.5-flash-image の停止は2026年10月2日です。ところが公式の推奨移行先として書かれている gemini-3.1-flash-image-preview は6月25日に停止済みでした。移行先を最後まで辿るスクリプトを書いて表の全行を検査し、同じ構造の行が11件あることを確かめた記録です。
API / SDK2026-09-11
文字起こしの固有名詞が直らないとき、カスタム語彙に何を入れて何を諦めるか
gemini-3.5-transcribe の custom_vocabulary は最大1,000語ですが、枠を埋めることが目的ではありません。誤認ログの正解と出力の対から、枠を渡す語だけを選び直す60行のスクリプトと、その判断基準をまとめます。
API / SDK2026-09-08
値上げの日をまたぐ費用の見積もりを、はじめから二段で書く
Gemini Flash の導入価格は2026年12月31日で終わり、2027年1月1日から標準価格に切り替わります。世代を据え置いても避けられないこの日を、はじめての費用見積もりでどう扱うか。動くコードとともに書き残します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます