「先月の Anthropic Console を開いたら、覚えのない時間帯にトークンが急増していて、気がついたら予算の3倍を使っていた」——本番で Claude API を運用していると、この種の事故は決して珍しくありません。私自身も、夜中に走り続けたバッチジョブが想定外のループに入り、翌朝請求金額を見て青ざめた経験があります。
問題なのは、コスト最適化の記事は無数にあるのに、「上限に達したら 強制的に止める 」仕組みを正面から扱った資料がほとんどないことです。プロンプトキャッシュや Haiku への切替で平均 コストは下げられますが、これらは予算事故そのものを防ぐ仕組みではありません。ここではClaude API の前段にサーキットブレーカーを置き、日次・月次の予算上限を確実に守る本番グレードのアーキテクチャを、Cloudflare Workers と Durable Objects を中心に組み立てていきます。
私はこれを4サイト(claudelab・rorklab・gemilab・antigravitylab)の運用に組み込んでいて、深夜に動くスケジュールタスクのトークン消費が突発的に跳ねた回も、上限に達した瞬間に静かに停止してくれて、翌朝のログで気づくレベルで済んでいます。今日は、その実装の中身と判断基準を順番に書いていきます。
なぜ「コスト最適化」だけでは予算事故を防げないのか
最初に整理しておきたいのは、コスト最適化と予算上限管理は別物だという点です。両者を混同していると、「最適化はしているのに事故は起きる」という状態から抜けられません。
私の運用経験で言えば、コスト最適化に投資した直後ほど油断が生まれます。「Haiku 切替で平均トークン単価を3割下げた、これでコスト管理は十分」という心理的安全感が、上限管理の遅れを招きます。実際には、平均が下がったぶんだけ「上振れ事故が起きたときの被害が見えにくくなる」副作用があり、対策を分けて考える必要があります。
コスト最適化の打ち手はすべて「1リクエストあたりの単価 」を下げる方向に働きます。プロンプトキャッシュ、モデルダウングレード、stop_sequences の活用、max_tokens のチューニング——いずれも単価を半分にしたり3割削ったりする力はありますが、リクエスト数 そのものが想定の10倍になれば、結局コストは数倍に膨らみます。
一方、予算事故の典型は次の3パターンです。
無限ループ系 : エージェントが終了条件を誤判定し、同じツール呼び出しを延々と繰り返す
バックグラウンド処理系 : バッチジョブやスケジュールタスクが冪等性を失い、翌日のジョブが前日のデータを再処理する
悪意あるアクセス系 : 認証の甘い API エンドポイントを発見されて、外部から大量にリクエストされる
これらはコスト最適化では止まりません。必要なのは、実消費量を計測し、閾値に達したら呼び出し自体を遮断する 仕組みです。これがサーキットブレーカーパターンと呼ばれているもので、もともとはマイクロサービスの障害伝播防止のために生まれた概念ですが、コスト管理にもそのまま応用できます。
サーキットブレーカーの3つの状態と Claude API への適用
電気回路のブレーカーが「通電」「遮断」の2状態を持つように、ソフトウェアのサーキットブレーカーには3つの状態があります。Claude API の文脈で整理してみましょう。
Closed(通常) : API 呼び出しを通過させる。実消費量を計測してストレージに記録する
Open(遮断) : API 呼び出しを遮断し、即座にフォールバック応答を返す
Half-Open(半開) : 一定時間経過後、試験的に1〜数リクエストだけ通して、リセット可否を判断する
Claude API のコスト管理では、Half-Open は基本的に「日次リセット」もしくは「月次リセット」に置き換えるのが現実的です。「予算超過したから60秒後に再試行」では意味がなく、「日次予算が回復するまで停止」が筋の通った設計です。
開発現場では、私はこの設計を以下のように具体化しています。
計測単位 : 入力トークン + 出力トークン、それぞれを別々のカウンタで記録(モデルごとに単価が違うため)
時間窓 : 日次(UTC 0:00 リセット)と月次(月初リセット)の2層
判定タイミング : API 呼び出しの 直前 に閾値チェックを行う(事後判定では遅い)
「事後判定では遅い」というのは特に重要なポイントで、「呼び出した後にカウンタを更新して、超えていたら次から止める」ようにすると、瞬間的な並列度が高い時に1回の判定で大量のリクエストが通り抜けてしまいます。並列度の高い本番環境ほど、予防的な事前ブロックが効きます。
最小実装 — Cloudflare Workers + KV で日次予算を守る
まず、最も小さな実装から始めましょう。これは1日あたりのトークン消費量を Cloudflare KV に記録し、閾値を超えたら API 呼び出しを拒否するパターンです。1万リクエスト/日くらいまでの中小規模なら、これで十分実用的に動きます。
// src/lib/budget-breaker.ts
import Anthropic from "@anthropic-ai/sdk" ;
const DAILY_TOKEN_CAP = 5_000_000 ; // 1日500万トークン上限(例)
interface Env {
ANTHROPIC_API_KEY : string ;
BUDGET_KV : KVNamespace ; // wrangler.toml で binding 済み
}
interface BudgetState {
inputTokens : number ;
outputTokens : number ;
date : string ; // YYYY-MM-DD
}
// UTC基準の日付キー(タイムゾーンを統一)
function todayKey () : string {
return new Date (). toISOString (). slice ( 0 , 10 );
}
// 現在の消費量を取得
async function readBudget ( env : Env ) : Promise < BudgetState > {
const key = `budget:${ todayKey () }` ;
const raw = await env. BUDGET_KV . get (key);
if ( ! raw) return { inputTokens: 0 , outputTokens: 0 , date: todayKey () };
return JSON . parse (raw) as BudgetState ;
}
// 事前チェック: 残額があれば true、なければ false
export async function checkBudget ( env : Env ) : Promise <{
allowed : boolean ;
used : number ;
remaining : number ;
}> {
const state = await readBudget (env);
const used = state.inputTokens + state.outputTokens;
return {
allowed: used < DAILY_TOKEN_CAP ,
used,
remaining: Math. max ( 0 , DAILY_TOKEN_CAP - used),
};
}
// 事後更新: 実消費量を記録(KV の TTL で2日後に自動削除)
export async function recordUsage (
env : Env ,
inputTokens : number ,
outputTokens : number
) : Promise < void > {
const key = `budget:${ todayKey () }` ;
const state = await readBudget (env);
state.inputTokens += inputTokens;
state.outputTokens += outputTokens;
await env. BUDGET_KV . put (key, JSON . stringify (state), {
expirationTtl: 60 * 60 * 48 , // 48時間後に自動削除
});
}
// ラッパー: API 呼び出しの前後で予算を管理
export async function callClaudeWithBudget (
env : Env ,
request : Anthropic . MessageCreateParams
) : Promise < Anthropic . Message | { error : "BUDGET_EXCEEDED" }> {
const budget = await checkBudget (env);
if ( ! budget.allowed) {
console. warn ( `[BUDGET] Blocked. used=${ budget . used } cap=${ DAILY_TOKEN_CAP }` );
return { error: "BUDGET_EXCEEDED" };
}
const client = new Anthropic ({ apiKey: env. ANTHROPIC_API_KEY });
const response = await client.messages. create (request);
// 実消費量を記録(input/output 別々に)
await recordUsage (
env,
response.usage.input_tokens,
response.usage.output_tokens
);
return response;
}
// 期待する出力例(remaining が 0 になった瞬間から、後続のリクエストは BUDGET_EXCEEDED を返す):
// { type: "message", id: "msg_01...", content: [{ type: "text", text: "..." }],
// usage: { input_tokens: 120, output_tokens: 480 } }
// もしくは予算超過時:
// { error: "BUDGET_EXCEEDED" }
この実装は シンプルで確実に動く のが最大の利点です。callClaudeWithBudget() を client.messages.create() の代わりに呼ぶだけで、既存コードへの変更は最小限で済みます。
ただし、この実装には注意すべき制約が1つあります。Cloudflare KV は 結果整合性 で、書き込みが世界中のエッジに反映されるまで最大60秒かかります。秒間100リクエストを超えるような高並列環境では、複数のリクエストが同時に「まだ予算がある」と判定して通過し、結果として閾値を10〜20%オーバーする可能性があります。後述する Durable Objects 版で、この問題を解決します。
高並列環境での厳密な制御 — Durable Objects による排他カウンタ
KV 版の限界を超えたい場合、Durable Objects(DO)が次の選択肢になります。DO は1つの ID に対して シングルスレッド・強整合 で動作するため、世界中からのリクエストを1つのキーに集約して、原子的なカウンタ更新が可能です。
// src/durable-objects/budget-tracker.ts
export class BudgetTracker {
private state : DurableObjectState ;
private cap : number = 5_000_000 ;
constructor ( state : DurableObjectState ) {
this .state = state;
}
async fetch ( request : Request ) : Promise < Response > {
const url = new URL (request.url);
if (url.pathname === "/check" ) {
const used = ( await this .state.storage. get < number >( "used" )) ?? 0 ;
return Response. json ({
allowed: used < this .cap,
used,
remaining: Math. max ( 0 , this .cap - used),
});
}
if (url.pathname === "/record" ) {
const { tokens } = await request. json <{ tokens : number }>();
// ⚠️ 重要: storage.transaction() で原子的に更新する
await this .state.storage. transaction ( async ( txn ) => {
const current = ( await txn. get < number >( "used" )) ?? 0 ;
await txn. put ( "used" , current + tokens);
});
return Response. json ({ ok: true });
}
if (url.pathname === "/reset" ) {
// 日次リセット用エンドポイント(Cron Trigger から叩く)
await this .state.storage. deleteAll ();
return Response. json ({ ok: true });
}
return new Response ( "Not Found" , { status: 404 });
}
}
// Worker 側: 単一の DO インスタンスにすべてのリクエストを集約
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const todayKey = new Date (). toISOString (). slice ( 0 , 10 );
const id = env. BUDGET_DO . idFromName ( `budget-${ todayKey }` );
const stub = env. BUDGET_DO . get (id);
// 事前チェック
const checkRes = await stub. fetch ( "https://do/check" );
const { allowed , used , remaining } = await checkRes. json <{
allowed : boolean ;
used : number ;
remaining : number ;
}>();
if ( ! allowed) {
return new Response (
JSON . stringify ({ error: "BUDGET_EXCEEDED" , used, remaining }),
{ status: 429 , headers: { "content-type" : "application/json" } }
);
}
// Claude API を呼び出す
const client = new Anthropic ({ apiKey: env. ANTHROPIC_API_KEY });
const response = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 1024 ,
messages: [{ role: "user" , content: "..." }],
});
// 実消費量を記録(原子的)
const totalTokens = response.usage.input_tokens + response.usage.output_tokens;
await stub. fetch ( "https://do/record" , {
method: "POST" ,
body: JSON . stringify ({ tokens: totalTokens }),
});
return Response. json (response);
} ,
} satisfies ExportedHandler < Env > ;
// 期待する挙動:
// - 100並列で同時に呼び出しても、合計トークンは正確にカウンタに加算される
// - 閾値超過した瞬間から、後続リクエストは即座に 429 BUDGET_EXCEEDED を返す
// - 閾値オーバーは最大1リクエスト分(並列で通過する瞬間のレース)で抑えられる
なぜ Durable Objects が必要なのか、トレードオフを言葉にしておきましょう。KV は 読み込みは速いが書き込みの整合性が遅い 、DO は 整合性は強いが単一インスタンスがボトルネック になります。秒間1,000リクエスト以下なら DO で問題なく捌けますし、それ以上なら DO を時間単位(例: 1時間ごとの ID)でシャーディングして、上位で集約する設計にします。中小規模 SaaS なら、まず DO を試して問題が出たらシャーディングに進むのが堅実です。
予算超過時の3つの戦略 — 全停止・劣化・通知のみ
サーキットブレーカーが Open になったとき、どう振る舞うか。これはビジネス要件によって大きく分かれます。私は3パターンに整理しています。
戦略A: 全停止(Hard Halt)
429 BUDGET_EXCEEDED を即座に返し、API 呼び出しを完全に遮断します。最もコストが守られますが、ユーザー体験は最悪です。バッチ処理・社内ツール・予算超過が即サービス停止より深刻な事業領域(医療・金融)に向きます。
戦略B: 劣化サービス(Graceful Degradation)
Claude API への呼び出しを遮断する代わりに、以下のいずれかを返します。
キャッシュ済みのテンプレート応答 (例: 「現在混雑しています。N分後に再度お試しください」)
より安価なモデルへフォールバック (Sonnet 4.6 → Haiku 4.5 へ自動切替)
ローカルの軽量処理で代替 (要約→単純な先頭N文字切り出し、感情分析→事前定義キーワードマッチ)
ユーザーが体験する破綻を最小化できますが、フォールバックロジックの保守コストがかかります。コンシューマー向けサービスではこれが第一選択です。
戦略C: 通知のみ(Alert-Only)
呼び出しは止めず、閾値を超えた瞬間に Slack / メールで管理者に通知します。「予算は守れないが、すぐ気づきたい」という、開発初期や信頼できる内部ユーザーのみ使う社内ツールに向きます。
実装上は、戦略Bが最も実用的です。コード例を示します。
// src/lib/budget-breaker-degraded.ts
type DegradationStrategy = "halt" | "fallback-model" | "cached-response" | "alert-only" ;
interface BreakerConfig {
cap : number ;
strategy : DegradationStrategy ;
fallbackModel ?: string ;
cachedResponse ?: string ;
webhookUrl ?: string ; // Slack Incoming Webhook
}
export async function callClaudeWithDegradation (
env : Env ,
config : BreakerConfig ,
request : Anthropic . MessageCreateParams
) : Promise < Anthropic . Message > {
const stub = getBudgetStub (env);
const checkRes = await stub. fetch ( "https://do/check" );
const { allowed , used } = await checkRes. json <{ allowed : boolean ; used : number }>();
// 閾値の80%を超えた時点で予防的アラート(戻り値は変更しない)
if (used >= config.cap * 0.8 && config.webhookUrl) {
fetch (config.webhookUrl, {
method: "POST" ,
body: JSON . stringify ({
text: `⚠️ Claude API budget at ${ Math . round (( used / config . cap ) * 100 ) }% (used=${ used } / cap=${ config . cap })` ,
}),
}). catch (() => {}); // 通知失敗は無視
}
if (allowed) {
return await callClaudeAndRecord (env, request);
}
// 予算超過時の分岐
switch (config.strategy) {
case "halt" :
throw new BudgetExceededError ( "Daily budget exhausted" );
case "fallback-model" :
// 安価モデルで再試行(フォールバックモデルにも別予算を持たせると堅牢)
return await callClaudeAndRecord (env, {
... request,
model: config.fallbackModel ?? "claude-haiku-4-5-20251001" ,
});
case "cached-response" :
// 事前用意のテンプレート応答を返す
return makeFakeMessage (
config.cachedResponse ?? "現在、応答の生成に時間がかかっています。"
);
case "alert-only" :
// 通知のみして、通常通り呼び出す
await notify (config.webhookUrl, `[BREAKER] cap exceeded but allowing through. used=${ used }` );
return await callClaudeAndRecord (env, request);
}
}
class BudgetExceededError extends Error {}
function makeFakeMessage ( text : string ) : Anthropic . Message {
return {
id: `msg_fallback_${ Date . now () }` ,
type: "message" ,
role: "assistant" ,
content: [{ type: "text" , text, citations: null }],
model: "fallback" ,
stop_reason: "end_turn" ,
stop_sequence: null ,
usage: { input_tokens: 0 , output_tokens: 0 , cache_creation_input_tokens: null , cache_read_input_tokens: null },
} as Anthropic . Message ;
}
// 期待する出力例:
// 通常時: 通常の Anthropic.Message
// 予算超過 + fallback-model: 同じ Anthropic.Message (model フィールドが Haiku になっている)
// 予算超過 + cached-response: { id: "msg_fallback_...", model: "fallback", usage: { ..._tokens: 0 } }
戦略選択の判断基準を整理すると、こうなります。
ユーザーが待つこと を許容できるか → Yes なら戦略B(cached-response で「N分後に再試行」を案内)
ユーザーが精度低下 を許容できるか → Yes なら戦略B(fallback-model)
内部ユーザーのみでコスト超過のリスク を取れるか → Yes なら戦略C(alert-only)
上記いずれも No → 戦略A(halt)
よくある間違い・落とし穴
ここまでの実装をそのまま動かしても、運用で以下の罠にハマることがあります。私自身が踏んだものを中心に、具体的な対処を書いておきます。
罠1: タイムゾーンの不一致による「日付またぎ事故」
「日次リセット」と書きましたが、new Date().toISOString().slice(0, 10) で得られる日付は UTC 基準 です。日本の運用感覚で「0時にリセットされるはず」と思っていると、実際は 9:00 JST にリセットされる挙動になり、想定外のタイミングで予算が回復したり、深夜帯のジョブが昨日と今日にまたがって2倍消費したりします。
対処は明示的なタイムゾーン処理です。日本時間で日付境界を切るなら、Intl.DateTimeFormat("ja-JP", { timeZone: "Asia/Tokyo" }) で書式化したキーを使うのが確実です。
罠2: usage フィールドを記録し忘れて「実は無計測」
client.messages.create() のレスポンスには usage フィールドがあり、ここに input_tokens / output_tokens が入っています。ストリーミングモード(stream: true)を使っている場合は、最後の message_delta イベントに usage が乗ってくるので、ここを取り損ねるとカウンタがゼロのまま動き続け、実質的にブレーカーが無効化されます。
ストリーミング時は次のように message_delta のハンドラで明示的に集計します。
// ストリーミング時のトークン記録
let inputTokens = 0 ;
let outputTokens = 0 ;
for await ( const event of stream) {
if (event.type === "message_start" ) {
inputTokens = event.message.usage.input_tokens;
}
if (event.type === "message_delta" ) {
outputTokens = event.usage.output_tokens;
}
// 各 chunk をクライアントに転送
yield event;
}
// ストリーム完了後に必ず記録
await recordUsage (env, inputTokens, outputTokens);
// 期待する挙動: ストリーミングが正常終了しても、途中切断(クライアント切断・タイムアウト)でも、
// 記録処理が呼ばれることを finally ブロックで保証する
罠3: プロンプトキャッシュ使用時の「キャッシュヒット分の二重計上」
プロンプトキャッシュを有効にすると、usage.cache_read_input_tokens と usage.cache_creation_input_tokens が入ってきます。これらは通常の input_tokens より単価が低い(キャッシュ読み込みは10%、書き込みは125%程度)ので、トークン数だけを足し算するとコスト計算が現実より大幅にずれます。
予算管理を金額ベース で行いたい場合は、各フィールドに重み付けして合算します。
// モデル別の単価(USD per 1M tokens、2026-04 時点の Sonnet 4.6 例)
const PRICING = {
inputPer1M: 3.0 ,
outputPer1M: 15.0 ,
cacheWritePer1M: 3.75 , // 書き込みは1.25倍
cacheReadPer1M: 0.3 , // 読み込みは1/10
};
function estimateCostUsd ( usage : Anthropic . Usage ) : number {
const input = (usage.input_tokens * PRICING .inputPer1M) / 1_000_000 ;
const output = (usage.output_tokens * PRICING .outputPer1M) / 1_000_000 ;
const cacheW = ((usage.cache_creation_input_tokens ?? 0 ) * PRICING .cacheWritePer1M) / 1_000_000 ;
const cacheR = ((usage.cache_read_input_tokens ?? 0 ) * PRICING .cacheReadPer1M) / 1_000_000 ;
return input + output + cacheW + cacheR;
}
// 単価は変動するため、最新値は Anthropic 公式の Pricing ページを参照すること
「金額ベースの予算」は実務的に最も意味があります。「1日あたり $50 まで」という上限のほうが、トークン数より直感的にステークホルダーへ説明できます。
罠4: ブレーカーを「ON / OFF」だけで考えて、運用フラグを忘れる
本番投入直後は、ブレーカーが誤って正常リクエストを止める可能性があります。リリース直後の数日は 観測のみモード (ログは記録するが遮断しない)で動かし、閾値の妥当性を確認してから遮断モードへ切り替えると安全です。これも環境変数や設定フラグで切り替えられるようにしておきます。
const BREAKER_MODE : "monitor" | "enforce" = (env. BREAKER_MODE as any ) ?? "monitor" ;
if ( ! allowed) {
if ( BREAKER_MODE === "enforce" ) {
return { error: "BUDGET_EXCEEDED" };
} else {
console. warn ( `[BREAKER] Would block (monitor mode). used=${ used }` );
// 通常通り通過させる
}
}
本番でブレーカーを正しく運用する3つの習慣
ブレーカー実装そのもの以上に重要なのが、運用の習慣化です。
第一に、閾値を「過去30日の P95」で決める こと。固定値で決めると、サービス成長に追いつけません。Anthropic Console や Cloudflare Analytics のデータをもとに、過去30日の日次消費量を計算し、その P95(または平均×1.5)を閾値の出発点にします。
第二に、毎月の「残量レポート」をチームに送る こと。月末に「予算を何%使ったか」「ブレーカーが何回発動したか」「どのエンドポイント / どの時間帯が消費の中心だったか」を可視化して関係者に共有します。これがないと、ブレーカーは「壊れているか正常かわからない箱」になり、結局誰も信頼しなくなります。
第三に、ブレーカー自体を E2E テストで検証する こと。具体的には、テスト用のモック KV / DO を使って「閾値の99%」「閾値ピッタリ」「閾値の101%」の3点で正しく挙動することを CI で確認します。本番事故が起きてからブレーカーのバグに気づく、という二重事故が一番苦しいので、ここは投資する価値があります。
第四に、ブレーカー発動を「学びの機会」として扱う こと。発動した時、すぐにブレーカーの閾値を上げて事なきを得るのは最も悪い対応です。なぜ閾値に達したのかを必ず分析し、無限ループの可能性、想定外のユーザー増、悪意あるアクセスのいずれかを見極めます。閾値を上げるのは原因が「健全な成長」だと判明したあとに限るべきです。私自身、急成長の早期段階では「閾値を上げ続ける運用」をしてしまい、バグによる暴走と健全な成長の区別がつかなくなった経験があります。
第五に、ブレーカーをチームの誰でも触れる仕組みにする こと。閾値の変更や戦略切り替えが「特定のエンジニアしかできない」状態は事故時に致命傷になります。Wrangler の Secrets / 環境変数で閾値を制御できるようにし、変更手順を Runbook に書いて、CS 担当者でも緊急時に閾値を引き上げられるようにしておきます。深夜のインシデントで担当エンジニアが寝ているとき、ブレーカーを誤発動の可能性込みで一時的に解除できる人が複数いると、サービス継続性が大きく変わります。
ダッシュボードと観測 — ブレーカーが動いていることを「目に見える状態」にする
ブレーカーを実装したら、その動作状況を可視化することが運用の半分を占めます。コードが正しくても、誰も状態を見ていなければ、サイレントに壊れている期間が長くなり、本番事故の時に「いつから動いていなかったのか分からない」という最悪の状態に陥ります。
私が実際に運用しているダッシュボードでは、最低限以下の4つのメトリクスを並べています。
当日の累積トークン消費量 (折れ線、当日 0:00 から現在まで)
過去30日の日次消費量 (棒グラフ、閾値ラインを赤線で重ねる)
ブレーカー発動回数 (数値、当日・今週・今月)
モデル別・エンドポイント別の内訳 (円グラフまたは積み上げ棒)
実装は Cloudflare Workers Analytics Engine が手軽です。各 API 呼び出し時に env.ANALYTICS.writeDataPoint() で1行記録するだけで、SQL ライクなクエリで集計できます。Datadog や Grafana Cloud と連携するなら、定期的に Workers から外部エンドポイントへ Push する形になります。
// Workers Analytics Engine への記録例
env. ANALYTICS . writeDataPoint ({
blobs: [
request.model ?? "unknown" , // blob1: モデル名
config.strategy, // blob2: 戦略
allowed ? "passed" : "blocked" , // blob3: 通過 / 遮断
],
doubles: [
response.usage?.input_tokens ?? 0 , // double1: 入力トークン
response.usage?.output_tokens ?? 0 , // double2: 出力トークン
estimateCostUsd (response.usage), // double3: 推定コスト
],
indexes: [ todayKey ()], // YYYY-MM-DD
});
// 集計クエリ例(Wrangler / GraphQL API 経由):
// SELECT blob1 AS model, SUM(double1+double2) AS tokens, SUM(double3) AS cost_usd
// FROM claude_api_dataset
// WHERE timestamp > NOW() - INTERVAL '24' HOUR
// GROUP BY blob1 ORDER BY tokens DESC;
このとき、ブレーカーが遮断したリクエストも必ず記録することがポイントです。「遮断された数」は単なる障害メトリクスではなく、サービス成長と予算設定のミスマッチを示す重要な経営指標になります。
複数モデル・複数チームを束ねるときの設計
サービスが大きくなると、「Sonnet 4.6 用の予算」と「Haiku 4.5 用の予算」を別々に管理したい、あるいは「フロントエンド機能の予算」と「バックエンドのバッチ処理の予算」を分けたいケースが出てきます。これを後付けで対応すると、コードのあちこちにブレーカー判定が散らばって保守が苦しくなります。最初から「予算スコープ」という概念を入れておくと、後の拡張が楽です。
具体的には、ブレーカーの呼び出し時に scope を渡せるようにして、Durable Object のキーをスコープ別に分けます。
// scope ごとに独立した予算カウンタを持つ
function getBudgetStub ( env : Env , scope : string ) : DurableObjectStub {
const todayKey = new Date (). toISOString (). slice ( 0 , 10 );
const id = env. BUDGET_DO . idFromName ( `budget-${ scope }-${ todayKey }` );
return env. BUDGET_DO . get (id);
}
// 使用例
await callClaudeWithBudget (env, "frontend-chat" , request);
await callClaudeWithBudget (env, "batch-summary" , request);
await callClaudeWithBudget (env, "internal-tools" , request);
スコープを切ることで、「フロントエンドが暴走してバッチ処理まで止まった」という連鎖障害を避けられます。逆に「全社共通の予算上限も同時に守りたい」場合は、スコープ別の判定に加えて、合算予算の判定も並列で行う2段階構造にします。
設計上の判断として、スコープを細かく切りすぎると「使い切れていないスコープがあるのに、別のスコープで遮断される」という非効率が生まれます。私の経験則では、3〜5個のスコープで運用するのが管理コストと柔軟性のバランスがちょうど良いと感じています。
全体を振り返って — 次に手を付けるべき1つのこと
ブレーカー全体を一気に作る必要はありません。今日帰る前に、まず 戦略Cの alert-only モード を1本入れてみてください。10行〜20行のコードで、当日の累積トークン数が予算の80%を超えた瞬間に Slack へ通知が飛ぶようにする、それだけで十分です。
これだけでも、「気がついたら予算超過」を「閾値の段階で気づける」状態に変えられます。本格的な遮断ロジックは、まず通知だけで運用してみて、誤検知が多いか少ないか、閾値が高すぎるか低すぎるかを把握してから実装すれば、運用コストが格段に下がります。
ブレーカーは「コストを最小化する道具」ではなく「想定外を起こさない道具」です。Claude API を本番で使う規模が大きくなるほど、その投資効果は大きくなります。
最後に1点だけ、運用面の心構えを書いておきます。サーキットブレーカーは「ある日突然サービスを止めるかもしれない仕組み」を自分のサービスの中に持つことを意味します。これは経営判断としても重い意思決定で、ビジネスサイドの理解なしに開発者だけで導入すると、いざ発動したときに社内で揉める原因になります。導入前に「どの戦略を採るか」「閾値をいくつにするか」「発動時に誰が判断するか」をビジネスサイドと合意しておくと、運用が驚くほどスムーズになります。技術的な実装よりも、この合意形成の方が時間がかかるかもしれませんが、その時間は確実にペイします。
関連する実装パターンとして、Claude API のレート制限とコスト最適化を本番運用に組み込むガイド では平均コストを下げる方向の打ち手を、Claude API でのリトライとフォールバックの本番設計 では障害時の振る舞いを、それぞれ深掘りしています。本記事と組み合わせて読むと、コスト管理・パフォーマンス・耐障害性の三つ巴を整理しやすくなります。
本記事のサーキットブレーカー設計と直接つながる考え方が多く、特に「アラート疲労を防ぐ閾値の決め方」は予算管理にもそのまま応用できます。