先日、弊社が運用するAIオウンドメディアの一つで、Gemini APIによる記事自動生成パイプラインが突然停止しました。原因は429エラー。Rate Limitに引っかかり、APIリクエストが弾かれまくっていたんですね。いや、これ、ほんと困る。
「おいおい、自動化してるはずが、結局俺が手動で監視・再実行するのかよ!」とツッコミたくなる状況です。
そこで今回は、この429エラーをスマートに乗り越え、パイプラインを安定稼働させるためのリトライ設計、特に指数バックオフの実装パターンについて、実際に僕がどうコードを書いて解決したのかを具体的に解説していきます。
この記事は、Gemini API(やその他のLLM API)で429エラーに悩まされている方、自動化システムの安定性を高めたい方に向けた実践的な内容です。
この記事でわかること
- Gemini APIにおける429エラー(Rate Limit)の基本的な理解
- なぜ単なるリトライでは不十分で、指数バックオフが必要なのか
- TypeScriptと非同期処理(
async/await)を使った指数バックオフの実装パターン - 実際にプロダクションで安定稼働させるためのリトライ設計のポイント
- 僕が運用するAI自動生成パイプラインの裏側で、どのようにエラーと戦っているか
1. そもそもGemini APIの429エラーとは何か?
429 Too Many Requestsは、APIサーバーが「君、ちょっとリクエストしすぎだよ」と教えてくれるHTTPステータスコードです。LLMのAPI、特にGenerative AIのAPIは、サーバー側の負荷を考慮して、一定時間あたりのリクエスト数(Rate Limit)を設けています。
Gemini APIも例外ではありません。ドキュメントには具体的な上限値が明記されていますが、実際にシステムを運用していると、想定以上のトラフィックが発生したり、他の処理と重なったりして、この上限に引っかかることが多々あります。
僕の場合、9言語展開でAIオウンドメディアを18サイトも一人で回していることもあり、Next.js + Gemini API + Vercel Cronで毎日記事を自動生成していると、どうしてもリクエストが集中する時間帯が出てきてしまいます。そうなると、当然のように429エラーを食らってしまうわけです。
問題は、このエラーを放置するとパイプラインが停止し、期待する記事が生成されないこと。
「自動運用」と謳っている以上、こういうエラーは極力自動でリカバリしたい。
2. なぜ単純なリトライではダメなのか?指数バックオフの必要性
「じゃあ、エラーが起きたらもう一度リトライすればいいじゃん?」
そう思いますよね。僕も最初はそう思いました。
しかし、単純なリトライ(例えば、1秒後に無条件で再試行)では、根本的な解決にはなりません。なぜなら、429エラーは「今、リクエストが多すぎる」という状況を示しているからです。すぐにリトライしても、またすぐに429エラーを食らう可能性が高い。むしろ、リトライを繰り返すことで、サーバーへの負荷をさらに高めてしまうリスクすらあります。
そこで登場するのが「指数バックオフ(Exponential Backoff)」です。
これは、リトライ間隔を試行回数に応じて指数関数的に長くしていく戦略です。例えば、
- 1回目の失敗:1秒待ってリトライ
- 2回目の失敗:2秒待ってリトライ
- 3回目の失敗:4秒待ってリトライ
- 4回目の失敗:8秒待ってリトライ
...といった具合です。
この戦略のメリットは、サーバー側の負荷を考慮しつつ、徐々に間隔を広げることで、いつかリクエストが通る「隙」を見つけ出すことにあります。まるで、相手の機嫌を伺いながら、タイミングを見計らって再アタックするようなものです。
「なんかこれって〜じゃない?」「429エラーって切り替えたから、この作業はもう不要かな?」と、常にスマートな解決策を模索する僕の思考にピッタリ合致した解決策でした。
3. TypeScriptで指数バックオフを実装する
では、実際にTypeScriptでどのように指数バックオフを実装するのかを見ていきましょう。
今回は、Gemini APIをコールする関数 callGeminiApi が失敗した場合に、指数バックオフでリトライするラッパー関数を考えます。
import { GoogleGenerativeAI } from '@google/generative-ai';
// Gemini APIクライアントの初期化(実際は環境変数などから取得)
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({ model: "gemini-pro" });
// APIリクエスト本体の関数(仮)
async function generateArticleContent(prompt: string): Promise<string> {
// 実際はもっと複雑なプロンプトや設定が入る
const result = await model.generateContent(prompt);
const response = await result.response;
const text = response.text();
console.log("Gemini APIからの応答:", text.substring(0, 50) + "..."); // 冒頭50文字だけ表示
return text;
}
/**
* 指数バックオフ付きリトライ処理でGemini APIを呼び出す関数
* @param apiCallFunc 実行したいAPI呼び出し関数
* @param maxRetries 最大リトライ回数
* @param initialDelayMs 初回待機時間(ミリ秒)
* @param exponentialBase 指数の底(例: 2で2倍ずつ)
* @returns 成功時のAPIレスポンス
*/
async function retryWithExponentialBackoff<T>(
apiCallFunc: () => Promise<T>,
maxRetries: number = 5,
initialDelayMs: number = 1000, // 1秒
exponentialBase: number = 2
): Promise<T> {
let retries = 0;
let delay = initialDelayMs;
while (retries < maxRetries) {
try {
// API呼び出しを試行
const result = await apiCallFunc();
return result; // 成功したら結果を返す
} catch (error: any) {
// エラーの種類をチェックし、リトライ対象か判断
if (error.status === 429 || (error.response && error.response.status === 429)) {
console.warn(`429エラーを検出。${delay / 1000}秒待機して再試行します。 (試行回数: ${retries + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay)); // 指定時間待機
delay *= exponentialBase; // 遅延時間を指数関数的に増加
retries++;
} else {
// 429以外のエラーは即座にスロー
console.error("Gemini API呼び出し中に予期せぬエラーが発生しました:", error);
throw error;
}
}
}
// 最大リトライ回数を超えたらエラーをスロー
throw new Error(`Gemini API呼び出しが${maxRetries}回のリトライ後も失敗しました。`);
}
// 実際に使用する例
async function main() {
const prompt = "日本の文化について、特にアニメや漫画に焦点を当てて記事を書いてください。";
try {
console.log("記事生成を開始します...");
const articleContent = await retryWithExponentialBackoff(
() => generateArticleContent(prompt),
10, // 最大10回リトライ
500, // 初回500ms待機
2 // 2倍ずつ増加
);
console.log("\n--- 記事生成成功 ---");
console.log(articleContent.substring(0, 300) + "..."); // 冒頭300文字だけ表示
} catch (error) {
console.error("記事生成に失敗しました:", error);
}
}
main();
コードの解説
-
generateArticleContent関数:- これはGemini APIを実際に呼び出す関数を模したものです。今回はプロンプトを受け取り、記事コンテンツを生成する想定です。
- 実際には、
model.generateContentの中に、安全性設定や温度設定、リトライなしでエラーハンドリングが必要な部分など、もっと多くのロジックが入ります。
-
retryWithExponentialBackoff関数:- この関数が指数バックオフの肝です。ジェネリック型
Tを使うことで、どんなAPI呼び出し関数にも対応できるようにしています。 -
maxRetries: 最大リトライ回数を設定します。無制限にリトライすると無限ループになる可能性があるため、上限は必須です。僕のAIオウンドメディアでは、日中であれば最大10回程度、夜間であればもう少し余裕を持たせて設定しています。 -
initialDelayMs: 初回のリトライ前に待機する時間です。短すぎるとすぐにまた429になりやすいので、少し余裕を持たせるのが吉です。 -
exponentialBase: 遅延時間を何倍にしていくかの基数です。一般的には2が使われますが、状況に応じて調整可能です。 -
while (retries < maxRetries): 指定されたリトライ回数に達するまでループします。 -
try...catchブロック:-
apiCallFunc()を実行し、成功すれば結果を即座に返します。 - エラーが発生した場合、
catchブロックに入ります。
-
-
if (error.status === 429 || ...): ここが重要です。エラーがHTTP 429 (Too Many Requests) であった場合にのみ、リトライ処理を行います。それ以外のエラー(認証失敗、プロンプトエラーなど)は、リトライしても解決しないため、すぐにthrow errorして処理を停止させます。- Gemini APIのJSクライアントライブラリは、エラーオブジェクトの構造が多少異なる場合があるので、
error.statusとerror.response.statusの両方をチェックするようにしています。これは「なんかこれって〜じゃない?」「Gemini APIって切り替えたから、この作業はもう不要かな?」と常にベストプラクティスを探す僕のこだわりですね。
- Gemini APIのJSクライアントライブラリは、エラーオブジェクトの構造が多少異なる場合があるので、
-
await new Promise(resolve => setTimeout(resolve, delay)): 指定された時間だけ非同期で待機します。これにより、Node.jsのイベントループをブロックせず、他の処理を妨げません。 -
delay *= exponentialBase: 次のリトライのために、遅延時間を指数関数的に増やします。 - 最大リトライ回数を超えた場合: ループを抜け、エラーをスローします。
- この関数が指数バックオフの肝です。ジェネリック型
運用上の注意点
- エラーログの充実: 何回目のリトライで成功したか、最終的に失敗した場合はどのようなエラーだったかをログに残すことで、将来的なデバッグや改善に役立ちます。
-
初期遅延と最大遅延:
initialDelayMsとmaxRetriesの設定は重要です。あまりにも長すぎるとユーザー体験を損ねますし、短すぎると効果が薄れます。システムとGemini APIの実際のトラフィックパターンを見ながら調整が必要です。僕のAIオウンドメディアでは、VercelのログやGoogle Search ConsoleのクロールステータスもAPI経由で自動収集し、データドリブンにこの辺りのパラメータをチューニングしています。 - サーキットブレーカーパターン: 極端にエラーが多い場合(例えば、Gemini API自体が障害を起こしている場合)には、リトライを停止して一定期間リクエストを中断する「サーキットブレーカーパターン」を導入することも検討します。これにより、無駄なリトライとサーバーへの追加負荷を防げます。
-
トークン消費量: リトライはAPI呼び出し回数を増やすため、当然ながらトークン消費量も増加します。予算と相談しつつ、
maxRetriesを設定することが重要です。
4. 実際のパイプラインでの活用
僕が運営するAIオウンドメディア18サイト(9言語展開・年間6,570記事)では、Next.jsとVercel Cronを使い、毎日記事を自動生成しています。この裏側で、上記で紹介した指数バックオフ付きリトライロジックがバリバリに稼働しています。
具体的には、
- 記事トピックの選定: Google Search Console APIやGA4データを元に、需要のあるキーワードや低順位だが改善の余地がある記事を検出。
- プロンプトの自動生成: 選定されたトピックに基づき、LLMが記事の構成や内容を指示するプロンプトを生成。
-
Gemini API呼び出し: 生成されたプロンプトを使って、
retryWithExponentialBackoffでラップされたgenerateArticleContent関数を呼び出し、記事コンテンツを生成。ここで429エラーが発生しても、自動でリトライしてくれる。 - 記事のリライト・最適化: 生成された記事は、さらに別のLLMエージェントによってSEO最適化、読解性の向上、著作権チェック(JASRAC管理楽曲の取り扱いなど、弊社はコンプライアンスを重視しています)などのプロセスを経て、最終的に公開可能な形に整形されます。
- 自動デプロイ: Next.jsのISR(Incremental Static Regeneration)機能とVercel Deploy Hookを使って、新しい記事を即座に公開。
- SNS自動投稿: 生成された記事は、その内容に合わせてLinkedIn, Note, Qiita, Zennなどの各SNSプラットフォームに最適化され、自動で投稿されます。
この一連のパイプラインの中で、特にGemini APIの呼び出し部分は負荷が高く、429エラーが発生しやすいポイントです。ここで指数バックオフが効くことで、パイプライン全体が滞りなく動作し、僕は「コアな創造的思考」に集中できています。
「弊社自身が使っている状態を作らないと、顧客に刺さらない」という哲学のもと、このシステムは自社メディアで徹底的に検証し、成果を上げています。そのノウハウを、MediaForge AIというB2B受託サービスとして展開し、企業の採用コンテンツ自動化SaaSやSEO自動化パイプライン構築も行っています(https://mediaforge-ai.vercel.app/)。
まとめ
Gemini APIをはじめとするLLM APIの運用において、429エラー(Rate Limit)は避けて通れない課題です。しかし、指数バックオフを適切に実装することで、システムの堅牢性を大幅に向上させ、安定した自動化パイプラインを構築できます。
今回紹介したTypeScriptでの実装パターンは、実際に僕が運用しているプロダクション環境で安定稼働しており、日々の記事自動生成を支える重要な要素です。
- 指数バックオフは、サーバーへの負荷を考慮しつつ、429エラーからの回復を目指す最適な戦略
- 最大リトライ回数、初期遅延、指数の底を適切に設定することが重要
- 429以外のエラーは即座に停止し、無駄なリトライを避ける
- 「自社実証ファースト」で培ったノウハウが、自動化システムの安定性を高める
この知見が、あなたのAIを活用した開発や自動化プロジェクトの一助となれば幸いです。
今回紹介したような、Next.js + Gemini APIを活用した先進的なシステムの構築代行や、既存のWordPressからNext.jsへの移行、SNS自動化パイプラインの構築なども行っています。ご興味があれば、ぜひ弊社ウェブサイトをご覧ください。
→ https://www.futuristicimagination.co.jp/service/
また、転職・副業・キャリアに関するShorts動画を毎日配信中です。日々の気づきやエンジニア経営者のリアルな視点をお届けしていますので、こちらもぜひチェックしてみてください。