月初に走らせた集計スクリプトの出力を見て、手が止まりました。前の請求期間、確保していたクレジットの18%を、ただ使い残したまま月をまたいでいたのです。
繰り越されていれば、ここまで気にはしませんでした。けれど6/15以降のヘッドレス実行は、月次クレジットが繰り越されません。使わなかった分は、月が変わった瞬間に消えます。逆に月の前半で勢いよく回しすぎれば、後半は full API レートの従量課金がそのまま乗ってきます。
個人開発で複数のサイトを自動運用していると、この「使いすぎても、使い残しても損」という非対称な落とし穴がじわじわ効いてきます。私自身、Dolice Labs として複数サイトを並行運用しているため、月次クレジットの配分は運用コストに直結する課題でした。今回は、残量と経過日数からその月の「使ってよい速度」を毎日決め直す、バーンレート配分スケジューラの設計を共有します。
残量が月末に2割も余っていた朝のこと
私自身が運用しているのは、定期的に走るヘッドレスのタスク群です。claude -p 形式の非対話実行を、時間帯を散らして一日に何本も回しています。
問題は、タスクごとにトークン消費がまったく一定でないことでした。短い整合性チェックは数千トークンで終わりますが、長文を扱う生成系は1回で数十万トークンに届きます。
月初は「まだ余裕がある」と感じて気前よく回し、月末が近づくと「足りなかったら困る」と無意識にブレーキを踏む。人間の感覚で運用していた結果が、あの18%の使い残しでした。安全側に倒しすぎて、確保した枠を活かしきれていなかったのです。
繰越なしの月次クレジットでは、この感覚的な運用がそのまま損失になります。消える前提の残量を、計画的に、しかし枯らさずに使い切る。そのためには感覚ではなく、残量と日付から逆算した数値が必要でした。
繰越なしクレジットがつくる二方向の損失
まず制約を整理します。6/15以降、Agent SDK・ヘッドレスの claude -p・GitHub Actions・サードパーティ製エージェントは、サブスクの利用上限とは別枠の月次クレジットへ移行しました。枠は契約に応じて $20(Pro)/ $100(Max 5x)/ $200(Max 20x)で、超過分は full API レートの従量課金、そして繰越なし です。
この「繰越なし」が、二方向の損失を生みます。
ひとつは早枯れ です。月の前半でバーンレートが高すぎると、枠を使い切ったあとの実行はすべて従量課金に流れます。固定で確保したはずのコストが、月末にかけて青天井に近づきます。
もうひとつは使い残し です。安全に倒しすぎると、月末に枠が余ったまま消えます。私が踏んだのはこちらでした。$200 の枠の18%、つまり $36 分を、毎月ただ捨てていた計算になります。
理想は、月末ぴったりで残量がゼロに近づく一定ペースです。けれど現実のタスクは消費量がばらつくため、固定スケジュールでは合わせきれません。だからこそ、毎日ペースを測り直して微調整する仕組みが要ります。
バーンレートで「使ってよい速度」を毎日決める
考え方はシンプルです。その日の朝に、次の3つを確認します。
残クレジット(その月にあと使える金額)
月末までの残り日数
今日すでに消費した金額
このとき、許容できる1日あたりの消費額は「残クレジット ÷ 残り日数」で求まります。これが今日の基準バーンレートです。そして今日の実消費がこの基準に達したら、その日の任意実行を止めます。
ポイントは、基準を毎日再計算する ことです。昨日少なめに終われば、その分が今日以降の残量に上乗せされ、自動的にペースが少し上がります。昨日使いすぎても、翌日以降の基準が自動的に下がって帳尻が合います。固定の日割りではなく、残量ベースの動的な日割りにするのがコツでした。
// burn-rate.ts — 残量と残り日数から、今日の許容消費を求める
export interface CreditState {
monthlyCredit : number ; // その月に確保した枠(USD)。例: 200
spentThisMonth : number ; // 今月ここまでの実消費(USD)
spentToday : number ; // 今日ここまでの実消費(USD)
now : Date ;
}
export interface PaceDecision {
remaining : number ; // 残クレジット
daysLeft : number ; // 今日を含む残り日数
dailyBudget : number ; // 今日使ってよい上限
todayHeadroom : number ; // 今日まだ使える余力
canRun : boolean ; // これ以上、任意実行してよいか
}
function daysLeftInMonth ( now : Date ) : number {
const y = now. getUTCFullYear ();
const m = now. getUTCMonth ();
const lastDay = new Date (Date. UTC (y, m + 1 , 0 )). getUTCDate ();
return lastDay - now. getUTCDate () + 1 ; // 今日を含める
}
export function decidePace ( s : CreditState ) : PaceDecision {
const remaining = Math. max ( 0 , s.monthlyCredit - s.spentThisMonth);
const daysLeft = daysLeftInMonth (s.now);
// 残量ベースの動的な日割り。昨日までの過不足が自動で反映される
const dailyBudget = remaining / daysLeft;
const todayHeadroom = dailyBudget - s.spentToday;
return {
remaining,
daysLeft,
dailyBudget,
todayHeadroom,
canRun: todayHeadroom > 0 ,
};
}
ここで monthlyCredit を full の枠より少し低めに置くと、月末に従量課金へなだれ込むリスクをさらに抑えられます。私は $200 の枠に対して $190 を基準枠にしています。残り $10 は、どうしても落とせない最優先タスクの安全マージンとして残しておく設計です。
実行前にトークンを見積もってキューを止める
バーンレートの基準が決まっても、実行を止める判断は「使ったあと」では遅すぎます。長文生成を1本走らせてから「枠を超えました」では、超過分はもう従量課金です。
そこで、キューから次のタスクを取り出す前に、そのタスクの消費を事前見積もり します。見積もりが今日の余力を超えるなら、そのタスクは翌日以降に回します。
見積もりは、タスク種別ごとの過去実測の中央値を使うのが現実的でした。平均ではなく中央値にしたのは、まれに混じる極端な大型実行に基準を引きずられないためです。
// estimate.ts — タスク種別ごとに、コストを事前見積もりする
type TaskKind = "integrity" | "shortgen" | "longgen" | "review" ;
// 過去実測の中央値(入力/出力トークン)。運用しながら更新する
const MEDIAN_TOKENS : Record < TaskKind , { input : number ; output : number }> = {
integrity: { input: 8_000 , output: 1_500 },
shortgen: { input: 40_000 , output: 12_000 },
longgen: { input: 180_000 , output: 60_000 },
review: { input: 90_000 , output: 8_000 },
};
// full API レート(USD / 100万トークン)。契約・モデルに合わせて更新する
const RATE = { input: 3.0 , output: 15.0 };
export function estimateCost ( kind : TaskKind ) : number {
const t = MEDIAN_TOKENS [kind];
return (t.input / 1_000_000 ) * RATE .input
+ (t.output / 1_000_000 ) * RATE .output;
}
この見積もりはあくまで中央値ベースの近似です。実行後は実消費を記録し、spentToday と spentThisMonth を実測値で更新します。見積もりで枠を管理し、実測で帳簿を合わせる。この二段構えが、繰越なし運用では効きました。
配分スケジューラの実装
ここまでの2つを組み合わせます。キューを優先度順に並べ、上から順に「今日の余力で実行できるか」を見て採否を決めます。
// scheduler.ts — バーンレートと事前見積もりでキューを捌く
import { decidePace, CreditState } from "./burn-rate" ;
import { estimateCost } from "./estimate" ;
interface Task {
id : string ;
kind : "integrity" | "shortgen" | "longgen" | "review" ;
priority : number ; // 小さいほど高優先
mustRun ?: boolean ; // 安全マージンを使ってでも必ず実行する
}
export function planToday ( queue : Task [], credit : CreditState ) {
const run : Task [] = [];
const deferred : Task [] = [];
// 優先度順に並べる
const sorted = [ ... queue]. sort (( a , b ) => a.priority - b.priority);
let spentToday = credit.spentToday;
for ( const task of sorted) {
const pace = decidePace ({ ... credit, spentToday });
const cost = estimateCost (task.kind);
// 最優先タスクは安全マージン込みの残量がある限り通す
if (task.mustRun && pace.remaining - cost > 0 ) {
run. push (task);
spentToday += cost;
continue ;
}
// 通常タスクは今日の余力の範囲だけ通す
if (pace.canRun && cost <= pace.todayHeadroom) {
run. push (task);
spentToday += cost;
} else {
deferred. push (task);
}
}
return { run, deferred, spentToday };
}
mustRun のタスクだけは、今日の日割り枠を超えても、月の残量がある限り通します。整合性チェックや障害検知のような「落とすと運用が崩れる」タスクを、ペース配分の都合で止めてはいけないからです。ペース配分はあくまで任意実行のためのもの、という線引きを明確にしました。
優先度を守りながら枠を使い切る
ここで悩ましいのが、月末の余力の扱いです。日割りどおりに止めていると、消費のばらつき次第で月末にわずかな残量が出ます。それを使い残せば、また同じ損失です。
そこで月末の数日だけ、ルールを少し緩めました。残り3日を切ったら、dailyBudget ではなく remaining(残量全体)を当日の余力として扱い、後回しにしていたタスクを優先度順に拾い直します。
// 月末スイープ: 残り3日を切ったら残量全体を使い切りにいく
export function endOfMonthSweep ( deferred : Task [], pace : { remaining : number ; daysLeft : number }) {
if (pace.daysLeft > 3 ) return [];
const picked : Task [] = [];
let budget = pace.remaining;
for ( const t of deferred. sort (( a , b ) => a.priority - b.priority)) {
const cost = estimateCost (t.kind);
if (cost <= budget) {
picked. push (t);
budget -= cost;
}
}
return picked;
}
この「月末スイープ」を入れてから、使い残しは目に見えて減りました。後回しにしていた品質改善系のタスクを月末にまとめて消化するので、捨てるはずだった枠が、そのまま記事の手直しに変わります。消えるだけだったクレジットが、成果に変わる瞬間でした。
計測してわかった、設計では気づけなかったこと
設計段階の想定と、実際に2か月回してわかったことには、いくつもずれがありました。
ひとつめは、見積もりの中央値は月の途中で更新しないと外れていく ことです。扱う題材が変われば消費量も動きます。私は週に一度、直近の実測から中央値を引き直すようにしました。これを怠ると、見積もりが実測から離れ、枠管理が甘くなります。
ふたつめは、mustRun を付けすぎると配分が機能しなくなる ことでした。最初は不安からあれもこれも必須にしていて、結果として全タスクが安全マージンを食い合っていました。本当に落とせないものだけに絞る。この見直しで、ようやく日割りが意味を持ちました。
みっつめは、タイムゾーンの扱い です。残り日数も「今日の消費」も、どの基準時刻でリセットするかで結果が変わります。請求の締めと、自分のログの日付がずれていると、月初・月末の1日分がまるごと二重計上・計上漏れになります。私はログの日付を必ず請求の締めと同じ基準に合わせました。
数値で言えば、月末の使い残しは18%から2%へ、超過による従量課金は月あたり数十ドル規模からほぼゼロへ動きました。確保した枠を、捨てもせず溢れさせもせず使い切る。地味ですが、繰越なしの制約下では一番効いた改善でした。
次に試すなら
もし同じ制約で運用を組み直すなら、まずは1日分の「許容バーンレート」をログに出すだけから始めることをお勧めします。decidePace の出力をログに流し、人間が見て止める運用を1週間。それだけで、自分の消費の癖が数値で見えてきます。
自動でキューを止めるのは、その癖が見えてからで十分です。最初から完全自動を目指すより、まず計測、次に半自動、最後に自動という順番のほうが、繰越なしの枠を安心して任せられる仕組みになっていきました。
同じように複数のヘッドレス実行を抱えている方の、設計のたたき台になれば幸いです。