Gemini API を使った機能を個人プロダクトに入れたあと、月末に請求書を見て息が止まる思いをしたことがあります。普段はほぼタダの範囲で回っていたのに、ある月だけ想定の 10 倍を超えてしまった、というやつです。原因を調べると、たいていは単一の派手なバグではなく、いくつかの小さな「気付けなかった穴」が重なって起きています。
ここでは私が自分の壁紙アプリの AI 機能、そして受託案件で Gemini を導入する支援の際に、コストの暴走を先回りで塞ぐために必ず入れているガードレールを、実装レベルで共有します。完璧なコスト管理は不可能ですが、「請求書を見てから驚く」構造は設計で消せます。
コスト暴走の 4 大原因を切り分ける
対策を書く前に、何が起きているのかを分解しておきます。Gemini API でコストが暴走するときは、私の経験上、次の 4 パターンのどれかです。
- 悪意のある呼び出し: API キーが漏れたり、認証が甘くて第三者が叩いている
- バグによる無限ループ: 失敗したリクエストをリトライし続ける実装が、エラーを正しく認識できず回り続けている
- ユーザー当たりの利用急増: 自分のプロダクトが Bot 的な使われ方をされ、特定ユーザーだけ大量消費している
- モデル変更やパラメータ変更の副作用: 開発者がモデルを Pro に切り替えた、
max_output_tokensを上げた、など
1 と 2 は「止めれば終わり」ですが、3 と 4 は正常動作の延長なので気付きづらいのが厄介です。ガードレールはこの 4 つを別々に受け止められる構成にしておきます。単一のクォータ設定だけでは足りないという前提に立つのが大切です。
まずはメトリクスを切り分けて取ります。
- 1 リクエスト当たりの平均トークン数(入力・出力別)
- 時間帯別のリクエスト数
- ユーザー(認証 ID or IP)当たりの 1 日の利用トークン数
- 使用中のモデル ID の分布
- リトライ発生率(全リクエスト中、何 % がリトライか)
これらを 時系列グラフで残すことが、コスト暴走を事後検知する最低条件です。私は個人プロダクトでも BigQuery か、簡単な SQLite + 時系列ダッシュボードでこの 5 指標だけは必ず取っています。
3 層のガードレール — ユーザー・日次・総額
私が入れているガードレールは、次の 3 層構造です。どれか 1 層だけだと、他のレイヤーから漏れが出ます。
| 層 | 対象 | 目的 |
|---|---|---|
| ユーザー単位 | 1 ユーザーの 1 日の消費量 | 乱用と特異値の検出 |
| 日次総量 | プロダクト全体の 1 日の消費量 | 一時的な異常の吸収 |
| 月次総量 | プロダクト全体の 1 ヶ月の消費量 | 最終的な青天井の遮断 |
ユーザー単位ガードレールの最小実装は、以下のような Redis カウンタで済みます。
async function checkUserQuota(userId: string, estTokens: number) {
const key = `gemini:quota:${userId}:${todayKey()}`;
const LIMIT_PER_USER_PER_DAY = 50_000; // トークン
const used = Number(await redis.get(key)) || 0;
if (used + estTokens > LIMIT_PER_USER_PER_DAY) {
throw new QuotaExceededError("user daily limit");
}
}
async function recordUsage(userId: string, tokens: number) {
const key = `gemini:quota:${userId}:${todayKey()}`;
await redis.incrby(key, tokens);
await redis.expire(key, 60 * 60 * 24 * 2);
}ポイントは 2 点です。まず、呼び出し前に予測トークンで仮減算をしておくこと。レスポンスが返ってきてから正確なトークン数に補正します。これをやらないと、大量リクエストが同時に来たときに、同時並行で上限を超えます。
もう 1 つは、expire で自動的に翌日にリセットすること。バッチ処理を書くとバグの温床になるので、キーの TTL に任せます。
日次総量と月次総量は、同じパターンでプロダクト全体のキーに対して実装します。ここでは Gemini API のレスポンスヘッダに含まれるトークン利用量を必ず採集し、正確な数字を積み上げます。見積りだけで運用すると、じわじわズレて月末にまた青ざめることになります。
サーバー側で「暴走するユーザー」を止める
3 層ガードレールのうち、ユーザー単位の層には、もう一段細かいロジックを入れておくと効きます。個人プロダクトでは、全ユーザーを一律の上限で縛るよりも、「普段と比べて明らかに多い」ことを検知して止めるほうが、体感の使い勝手を壊しにくいからです。
実装としては、直近 7 日の中央値を覚えておいて、今日の使用量がその 5 倍を超えた瞬間にスローダウンする、という形にしています。
async function detectAnomaly(userId: string, used: number) {
const median7d = await getUserMedian7d(userId);
if (median7d && used > median7d * 5 && used > 10_000) {
// 異常と判定:レートリミットを厳しく適用し、Slack に通知
await applyCooldown(userId, "suspected_abuse");
await notifyOps(`user ${userId} using ${used} tokens (median7d=${median7d})`);
}
}「絶対値 10,000 トークン以上」という下限を併用しているのは、そもそも普段ほぼ使わないユーザーが一度だけ多めに使ったのを誤検知しないためです。倍率だけで判定すると、ゼロに近い母数からの急増がすべて異常扱いになってしまうので、実運用では必ず絶対値の下限とセットで判定します。
この異常検知で一時的にスローダウンをかけるのは、ユーザーへの明示的なエラーより柔らかい手段です。「しばらくお待ちください」と表示しつつ、実際には 1 分あたりの上限を厳しくする、といった運用にしておくと、真っ当なユーザーは気付かないうちに戻ります。
モデル変更とパラメータ変更を「事故」にしない
コスト暴走の 4 つ目の原因、モデルやパラメータの変更は、コードレビュー時に気付けない類の事故を生みます。gemini-flash から gemini-pro に切り替えたコミットが、本人も気付かないうちにコストを 10 倍近く跳ね上げる、というのはあり得ます。
私が入れているルールは、次の 2 点です。
- モデル名とトークン上限は、ソースコードに直書きせず、単一の設定ファイルに集約する
- その設定ファイルを変更する PR は、コスト影響を本文に必ず書く
設定ファイルの例はこれくらい簡潔で十分です。
# config/gemini.yaml
defaults:
model: gemini-2.5-flash
max_output_tokens: 1024
temperature: 0.3
features:
wallpaper_caption:
model: gemini-2.5-flash
max_output_tokens: 256
daily_summary:
model: gemini-2.5-pro
max_output_tokens: 2048こうしておくと、「wallpaper_caption を pro に変更」という差分が、それ単体で PR の本体として可視化されるため、レビュアーがコスト影響を見落としにくくなります。さらに、CI 側でこの YAML を読んで「前回のデフォルトと比較して、どの機能のモデルが変わったか」を自動コメントするようにしておくと、事故率はもう一段下がります。
請求書の前に気付くための異常検知通知
最後の砦として、毎日のコストを自動で可視化・通知する仕組みを必ず入れます。Google Cloud の予算アラートも使えますが、単独だと「閾値を超えた瞬間に通知」なので、暴走の最中にしか気付けません。より細かい異常検知は、自前で軽く組んだほうが安心です。
私が使っているのは、1 時間ごとにコスト推移を集計して、直近 7 日の同じ時間帯の中央値と比較するシンプルなジョブです。
async function hourlyCostCheck() {
const now = new Date();
const hour = now.getHours();
const currentCost = await sumCostLast1h();
const median = await medianCostAtHour(hour, 7);
if (currentCost > median * 3 && currentCost > 1_00) {
// $1 を超える × 中央値の 3 倍 で通知
await sendSlack(
`Gemini hourly cost anomaly: $${(currentCost / 100).toFixed(2)} (median=$${(median / 100).toFixed(2)})`
);
}
}通知先は Slack、LINE、メールなど好きな経路で構いません。大切なのは、閾値を「絶対額」と「倍率」の 2 段で持つことです。絶対額だけだと日中の正常増加で誤検知が増え、倍率だけだと深夜の少額スパイクで騒がしくなります。両方を AND で使うのがちょうどいい感覚です。
コストを「気合いで節約する」時代は終わっている
Gemini API のコストは、プロンプトを短くすることや、キャッシュを入れることで削れます。ただ、それは「うまく動いている日」の節約であって、暴走の日には効きません。個人開発者にとって、月末に 10 倍の請求書が来る 1 日は、節約で稼いだ数ヶ月分を吹き飛ばすインパクトがあります。
だからこそ、コスト設計はプロンプト最適化と切り離して、ガードレール側で独立に設計すべきだと思っています。ユーザー単位の上限、日次・月次の総量、モデル変更の可視化、そして毎時の異常検知。これらを揃えておけば、コストに関する夜の不安はかなり軽くなります。
個人プロダクトで Gemini を本格的に使い始めたばかりの方は、まずユーザー単位の上限と、毎時の異常検知の 2 つだけでも入れてみてください。請求書を恐れずに、プロダクトの本体に集中できる時間が増えていきます。