ある朝、動かしていた3つの実験のうち2つが黙って止まっていました。片方は壁紙アプリの画像分類バッチ、もう片方は記事を音声化する下処理で、どちらも 429 RESOURCE_EXHAUSTED を返し続けていました。原因は3つ目の実験でした。前夜に仕込んだ埋め込みの再インデックスにループの止め忘れがあり、ひと晩でその月の予算のおよそ7割を溶かしていたのです。
痛かったのは、暴走した実験そのものではなく、無関係だった2つまで一緒に止まったこと でした。3つを同じ請求アカウントの、同じプロジェクトに同居させていたので、上限に当たった瞬間、口座ごと息が止まったわけです。
2026年7月、AI Studio にプロジェクト単位の費用上限(spend caps)が入りました。これは単なる「使いすぎ防止」ではなく、並行する実験を互いに切り離すための隔離境界 として設計できる道具だと考えています。ここでお伝えするのは、その境界の引き方と、ハード上限だけでは足りない部分をクライアント側でどう補うかです。すべて実装込みで進めます。
「口座単位の上限」では実験を隔離できない
これまでのコスト上限は、多くの場合アカウント全体にかかるものでした。合計額でしか止められないと、どの実験が食っているかに関係なく、限界に達した瞬間にすべてが同時に落ちます。
個人開発だと、この構造は日常的に牙をむきます。私の手元では、安定運用中のアプリ機能と、まだ挙動の読めない実験が、同じ財布の中で肩を並べています。安定運用中の機能の多くは、AdMob の広告収益で支えている無料アプリの一部です。実験は本来いちばん暴れやすいものですから、その暴走が本番機能を巻き込む設計は、順序が逆さまです。守りたいものほど無防備になっています。
必要なのは「合計いくらまで」ではなく、「この実験は最大でもここまで 」という個別の壁です。プロジェクト費用上限は、まさにこの粒度を与えてくれます。
プロジェクトを実験の単位に切り替える
隔離の第一歩は、プロジェクトの切り方を変えることです。私は次の原則に寄せました。
安定運用の本番機能はひとつのプロジェクトにまとめ、確実に守る。一方で、挙動の読めない実験はひとつにつきひとつのプロジェクト へ分け、それぞれに API キーと費用上限を割り当てる。こうしておくと、実験Aがどれだけ暴れても、上限に当たるのは実験A自身のプロジェクトだけで、本番も実験Bも無傷のまま走り続けます。
プロジェクトを分ける手間は、キーの管理が増えるぶん確かにあります。それでも、一晩の事故で本番まで止める痛みに比べれば、はるかに軽い代償でした。
防御は一枚ではなく層で組む
プロジェクト費用上限は強力ですが、これ単体に頼ると別の困りごとが出ます。ハード上限は文字通りの「壁」なので、当たった瞬間に処理が途中で断ち切られます。バッチの半分だけが完了して残りが失われる、といった中途半端な失敗を招きやすいのです。この落とし穴を回避するには、停止そのものを避けるより、停止の仕方をあらかじめ設計しておくほうが効きます。
そこで、上限を一枚の壁ではなく、性質の違う層として重ねます。
層 役割 効き方
AI Studio ハード上限 最後の砦 突然・強制的に停止(想定外の暴走のみを受け止める)
クライアント側ソフト天井 先に効く安全弁 呼び出し前に静かに縮退・保留する
縮退動作 止まらずに減速 キャッシュ・軽量モデル・翌月キューへ退避
月次照合 ズレの補正 台帳の推定を実請求と突き合わせる
ハード上限はあくまで「そこまで来たら異常」という最終ラインに置き、日常の減速はソフト天井が担う。この役割分担が、中途半端な失敗を避ける鍵になります。
実装: プロジェクト別の予算台帳とソフトゲート
ソフト天井の中身は、呼び出しごとに概算コストを積み上げる小さな台帳です。呼ぶ前に「この一回で天井を超えないか」を判定し、超えるなら例外を投げて処理側に縮退の判断を返します。
import threading
from dataclasses import dataclass, field
# Gemini 3.5 Flash の概算単価(USD / 100万トークン)。実値は必ず請求ページで確認してください。
PRICE_PER_MTOK = { "input" : 0.075 , "output" : 0.30 }
def estimate_cost_usd (input_tokens: int , output_tokens: int ) -> float :
return (input_tokens * PRICE_PER_MTOK [ "input" ]
+ output_tokens * PRICE_PER_MTOK [ "output" ]) / 1_000_000
class BudgetExhausted ( Exception ):
"""ソフト天井に達したことを処理側へ伝える。ハード上限より先に投げる。"""
@dataclass
class ProjectBudget :
project_id: str
hard_cap_usd: float # AI Studio 側のハード上限と必ず一致させる
soft_ratio: float = 0.8 # ハード上限の80%をソフト天井にする
spent_usd: float = 0.0
_lock: threading.Lock = field( default_factory = threading.Lock, repr = False )
@ property
def soft_cap_usd (self) -> float :
return self .hard_cap_usd * self .soft_ratio
def reserve (self, projected_usd: float ) -> None :
with self ._lock:
if self .spent_usd + projected_usd > self .soft_cap_usd:
raise BudgetExhausted(
f " { self .project_id } : soft cap { self .soft_cap_usd :.2f } USD 到達"
f "(消費 { self .spent_usd :.2f } + 見込み { projected_usd :.4f } )" )
def commit (self, actual_usd: float ) -> None :
with self ._lock:
self .spent_usd += actual_usd
呼び出しは、この台帳をくぐらせてから実行します。入力トークンは count_tokens で実測し、出力は上限値で保守的に見積もって予約します。
def guarded_generate (client, budget: ProjectBudget, * , model, contents, max_output_tokens):
# 最悪ケース(出力上限まで使う想定)で予約してから呼ぶ
input_tokens = client.models.count_tokens( model = model, contents = contents).total_tokens
projected = estimate_cost_usd(input_tokens, max_output_tokens)
budget.reserve(projected) # 天井を超えるなら BudgetExhausted で即座に止まる
resp = client.models.generate_content(
model = model, contents = contents,
config = { "max_output_tokens" : max_output_tokens})
usage = resp.usage_metadata
actual = estimate_cost_usd(usage.prompt_token_count, usage.candidates_token_count)
budget.commit(actual) # 実消費で台帳を更新する
return resp
以前の私のコードは、次のように 429 を握って指数バックオフで叩き直すだけでした。
# Before: 上限に当たっても「待てば直る」と信じて叩き続ける
for attempt in range ( 7 ):
try :
return client.models.generate_content( model = model, contents = contents)
except ResourceExhausted:
time.sleep( 2 ** attempt) # 予算枯渇のときは何回待っても通らない
この形は、レート制限(待てば直る)と予算枯渇(月が変わるまで直らない)を区別できません。枯渇したプロジェクトに対して7倍の無駄打ちを続け、レイテンシだけが膨らみます。予算枯渇と一過性の制限を見分ける設計は、Gemini API の 429 を全部リトライしてはいけません で詳しく扱いました。あわせて読んでいただけると、リトライ層とソフト天井の役割分担が立体的になります。
guarded_generate を使う側は、BudgetExhausted を捕まえて縮退させます。
try :
resp = guarded_generate(client, budget, model = "gemini-flash-latest" ,
contents = chunk, max_output_tokens = 1024 )
except BudgetExhausted:
queue_for_next_month(chunk) # 止めずに退避し、本番機能の予算は温存する
止めるのではなく、退避させる。ここが「巻き添えを避ける」設計の要です。実験が自分の天井に当たっても、それはその実験のキューが伸びるだけで、隣の本番機能には一切届きません。
台帳の推定と実請求のズレをどう吸収するか
台帳はあくまで概算です。入力トークンは count_tokens でほぼ正確に取れますが、出力は上限値で予約しているため、実際の消費よりやや多めに積み上がります。この保守的なズレは、事故の方向としては安全側です。使いすぎるより、少し早めに天井へ当たるほうが望ましいからです。
私の環境では、台帳の推定と実請求のズレは月あたり約3〜5%に収まりました。ソフト天井をハード上限の80%に置いているのは、この差分と、複数スレッドから同時に予約が入るときの取りこぼしを吸収するためです。残りの20%が、ハード上限に到達させないための緩衝地帯になります。
月に一度は、実請求の値を取り込んで台帳の初期値を補正します。count_tokens のオーバーヘッドは私の計測で1回あたり数十ミリ秒でしたので、バッチ全体では無視できない量になります。頻繁に短い呼び出しを繰り返す用途では、入力トークン数を自前で見積もってこの一手を省く選択も現実的です。コスト暴走そのものの切り分けについては、Gemini API のコスト暴走を防ぐガードレール設計 に4大原因の分解を書いていますので、台帳を組む前の地図として役立つはずです。
どこから手をつけるか
まず、いちばん挙動の読めない実験をひとつだけ、専用プロジェクトに切り出してみてください。API キーを分け、AI Studio でそのプロジェクトに費用上限を設定する。これだけで「この実験は最大でもここまで」という壁が一枚立ちます。予算の読めない処理を抱えている方には、まずこの一手だけでも進めることを強く推奨します。
ソフト天井の実装は、その次で構いません。プロジェクトを分けた時点で、暴走の巻き添えという最悪のパターンはすでに断ち切れています。台帳とソフトゲートは、中途半端な失敗を減らし、止める代わりに減速させるための上積みです。
なお、無料ユーザーのコストが粗利を食う本番アプリ側の天井設計は、無料ユーザーのコストが粗利を食う前に で別途まとめています。今回の「開発者自身の実験を隔離する」話とは守る対象が違いますので、本番を持っている方は両輪で考えていただくとよいと思います。
私自身、一晩の事故で本番まで止めた日のことは、いまも設計の物差しになっています。実装の参考になれば幸いです。お読みいただき、ありがとうございました。