6月の最終週、私自身の定期実行を1日40回から36回へ整理したばかりでした。使用量が想定を超えていたためで、どのタスクを残すかをかなり慎重に選んだ記憶が新しいところです。その直後に、Claude Code を含む各プロダクトの週次上限が7月13日まで50%引き上げられるという知らせが届きました。減らした翌週に増える、という間の悪さに少し笑ってしまいましたが、増えた枠を眺めて最初に浮かんだのは「積み残しの一括処理に使えるのではないか」ということでした。
ただし今回の引き上げには期限があります。7月13日を過ぎれば枠は元に戻ります。この「期限付き」という性質を軽く見ると、期限の翌朝に自動運用が壊れます。ここで共有するのは、期限付きの引き上げ枠を日常の処理量(基線)に混ぜずに、終わりのある仕事だけを流し込むための設計です。動くコードと、個人開発の現場で運用中の途中経過をあわせて置いておきます。
期限付きの引き上げは「借りた枠」です
恒久的な引き上げと期限付きの引き上げは、数字の上では同じ「+50%」でも運用上はまったくの別物です。
基線、つまり毎日・毎週決まって流れる処理量を、引き上げ後の枠に合わせて150%に慣らしてしまったとします。7月14日に枠が100%へ戻った瞬間、基線を約33%削る必要が生じます(150 → 100 は 50/150 = 33.3% の削減です)。私のように定期タスクが数十本ある環境では、33%の削減は「どの3本に1本を止めるか」という選別作業そのものであり、6月末に一度やった苦しい整理をもう一度、今度は準備期間なしでやることになります。
一方、基線を据え置いて増分の50%だけを別枠として扱えば、期限日に何かを削る必要はありません。別枠に入れた仕事が終わるか、期限が来て別枠が閉じるか、どちらか早いほうで自然に終わります。
6月にレート上限が恒久的に倍増したときは、増えた余裕を再試行のヘッドルームに回して間隔は詰めない、という判断をしました(そのときの経緯はレート上限が倍増しても自動運用の間隔は詰めないに書いています)。恒久増なら「安定性に変える」が答えでしたが、期限付きの場合は話が逆で、安定性の設計に組み込んではいけません。期限とともに消える余裕を前提にした安定性は、期限とともに消えるからです。
窓に入れてよい仕事と、入れてはいけない仕事
では増えた枠で何を流すか。判定基準は「その仕事に終わりがあるか」の一点です。私は次の3つの質問で仕分けています。
| 質問 | はい → 窓に入れる候補 | いいえ → 窓に入れない |
|---|
| 作業量は有限で、完了が定義できるか | 蓄積した統合キューの消化、過去記事の内部リンク一括監査、依存パッケージの棚卸し | 日次の記事生成、監視・巡回 |
| 期限までに終わらなくても、翌日の運用に支障がないか | 残った分を通常の低優先レーンへ戻せる仕事 | 途中で止まると不整合が残る移行作業 |
| 期限後に「続けたい」と思わずにいられるか | 一度きりの点検・清算系 | 実行間隔の短縮、品質ゲートの追加実行など、効果を体感すると戻せなくなる系 |
3つ目の質問が実は一番重要だと考えています。窓の期間中に「生成間隔を詰めたら品質チェックの回数が増えて安心感があった」というような体験をしてしまうと、期限後にそれを手放すのが心理的に難しくなります。設定は戻せても、習慣は戻しにくい。だから最初から、習慣になり得る種類の仕事は窓に入れないのが安全です。
私の環境での具体例を挙げると、窓に入れたのは「削除済み記事の統合キューに溜まっていた31件の処理」「4サイト分の内部リンク一括監査」「参照データの過去分の整合点検」の3種類で、いずれも件数が数えられて完了が定義できる仕事です。逆に、毎日の記事生成やブラッシュアップの本数はいっさい変えていません。
期限を知っているバーストキュー
仕分けができたら、次は実装です。やってはいけない実装から先に示します。
# Before: cron に「一時的な」エントリを直接足す
# 増枠が来たので統合処理を1日3回に増やす(つもり)
0 9,14,21 * * * /usr/local/bin/run-integration-batch.sh
これが悪手である理由は2つあります。第一に、7月14日に消し忘れます。cron のエントリには「これは期限付き」というメタデータを持たせられないので、消す作業は人間の記憶頼みになります。第二に、実行ログの上で基線の処理とバーストの処理が区別できなくなり、後述する「漏れ膨張」の検出ができなくなります。
代わりに、期限を自分で知っているキューを1枚かませます。
// After: 期限を知っているバーストキュー
// - 期限を過ぎたら新規ジョブを受け付けない
// - 完了見込みが期限を超えるジョブは入口で拒否する
// - 一回性ジョブの種別だけを許可リストで通す
type BurstJobKind =
| "integration-queue" // 統合キューの消化
| "link-audit" // 内部リンク一括監査
| "reference-consistency"; // 参照データ整合点検
interface BurstJob {
id: string;
kind: BurstJobKind;
estimatedMinutes: number; // 過去の同種ジョブの実測から見積もる
}
const ALLOWED_KINDS: ReadonlySet<BurstJobKind> = new Set([
"integration-queue",
"link-audit",
"reference-consistency",
]);
export class BurstWindow {
private queue: BurstJob[] = [];
constructor(private readonly expiresAt: Date) {}
/** 入口での受け入れ判定。理由付きで返すとログが読みやすくなります */
admit(job: BurstJob, now: Date = new Date()): { ok: boolean; reason: string } {
if (now >= this.expiresAt) {
return { ok: false, reason: "window-expired" };
}
if (!ALLOWED_KINDS.has(job.kind)) {
// 繰り返し系のジョブ種別はコンパイル時ではなく実行時にも弾く
return { ok: false, reason: `kind-not-allowed: ${job.kind}` };
}
const remainingMinutes =
(this.expiresAt.getTime() - now.getTime()) / 60_000;
if (job.estimatedMinutes > remainingMinutes) {
// 期限をまたぐ見込みのジョブは最初から入れない。
// 「途中まで進めば得」に見えても、中断状態の後始末コストのほうが高くつきます
return { ok: false, reason: "would-cross-expiry" };
}
this.queue.push(job);
return { ok: true, reason: "admitted" };
}
/** 期限到達後は残りを通常レーンへ払い出して空にする */
drainAfterExpiry(now: Date = new Date()): BurstJob[] {
if (now < this.expiresAt) return [];
const leftovers = [...this.queue];
this.queue = [];
return leftovers; // 呼び出し側で低優先の通常レーンへ積み直す
}
}
ポイントは admit() が3段構えになっていることです。期限そのもの、ジョブ種別の許可リスト、そして完了見込みと残り時間の比較。特に3つ目は見落としがちで、期限の前日に6時間かかる監査ジョブを投入すると、期限をまたいだ中途半端な状態が残ります。中断された監査は「どこまで見たか」の記録がなければ最初からやり直しになるので、入口で拒否するほうが総コストは安くつきます。
基線とバーストを別々に記帳する
キューを分けたら、記帳も分けます。目的は期限後の点検です。窓の期間中に基線側の消費が知らないうちに増えていないか、そして期限後に基線が窓の前の水準へ戻っているか。この2つを確認できて初めて「借りた枠を返した」と言えます。
// 実行1件ごとに lane を付けて記録する最小の台帳
type Lane = "baseline" | "burst";
interface RunRecord {
at: Date;
lane: Lane;
taskId: string;
}
export class LaneLedger {
private records: RunRecord[] = [];
record(taskId: string, lane: Lane, at: Date = new Date()): void {
this.records.push({ at, lane, taskId });
}
/** 指定期間の lane 別実行数 */
countByLane(from: Date, to: Date): Record<Lane, number> {
const out: Record<Lane, number> = { baseline: 0, burst: 0 };
for (const r of this.records) {
if (r.at >= from && r.at < to) out[r.lane] += 1;
}
return out;
}
/**
* 漏れ膨張の点検: 窓の前の週を1.00として、対象週の基線実行数の比を返す。
* 1.05 を超えたら「基線に何かが混ざった」サインとして警告します
*/
baselineDriftRatio(refWeek: [Date, Date], targetWeek: [Date, Date]): number {
const ref = this.countByLane(...refWeek).baseline;
const target = this.countByLane(...targetWeek).baseline;
if (ref === 0) return Number.POSITIVE_INFINITY;
return target / ref;
}
}
点検は次の手順で行います。
- 窓が始まる前の1週間の基線実行数を基準値(1.00)として控えます
- 窓の期間中、基線の比率が 1.05 を超えていないかを毎日確認します。超えていたら、バーストに入れるべき仕事が cron 側に紛れ込んでいます
- 期限の翌週、もう一度同じ比率を取り、1.00 近辺に戻っていることを確認します
drainAfterExpiry() が払い出した残りジョブを、通常の低優先レーンに積み直します。「次の引き上げを待つ」という選択肢は取りません。次があるかは分からないからです
しきい値を 1.05 にしているのは、私の環境では曜日構成の揺らぎ(週次タスクの有無)で ±3% 程度は自然に変動するためです。ご自身の環境の平常時の揺らぎを1〜2週間分測ってから決めることをお勧めします。
私の環境での途中経過 — 窓の3日目まで
この記事を書いている時点で窓の3日目です。個人開発で運営している技術ブログ群の自動運用に、上の仕組みをそのまま入れて動かしています。途中経過の数字を置いておきます。
| 項目 | 窓の開始前 | 3日目時点 |
|---|
| 統合キューの残件 | 31件 | 17件(14件処理) |
| 内部リンク一括監査 | 4サイト未着手 | 2サイト完了・リンク切れ12件検出 |
| バースト実行数 / 週次枠に対する消費割合 | — | 19回 / 概算22% |
| 基線の実行数(前週比) | 1.00 | 1.03 |
基線の 1.03 は先ほどの揺らぎの範囲内で、バーストの消費は増分50%のうちの半分弱に収まっています。残り10日で統合キューの17件は消化できる見込みですが、リンク監査の残り2サイトは estimatedMinutes の見積もりが期限に対してぎりぎりで、admit() に弾かれる可能性があります。弾かれたら、それは通常レーンで淡々と進める仕事に戻るだけです。この「弾かれても困らない」状態を保てているのは、窓に入れたのが最初から終わりのある仕事だけだからだと感じています。
なお、繰越のない月次クレジットを月内でどう配分するかという近い問題を以前に扱いました(繰越されない月次クレジットを月初に枯らさず月末に余らせないバーンレート配分)。あちらは「使い切る」ための配分、今回は「基線に混ぜない」ための隔離で、最適化の方向が逆である点は使い分けの目安になるかと思います。
7月14日の朝に、何も変えなくて済むように
期限付きの引き上げ枠に対する私の結論はシンプルです。基線は動かさない。窓には終わりのある仕事だけを、期限を知っているキュー経由で入れる。記帳を分けて、期限後に基線が元の水準にあることを比率1つで確認する。これだけで、期限の翌朝にやることは「台帳の比率を1回見る」だけになります。
次のアクションとしては、まずご自身のタスク一覧を「終わりがあるか」の一点で仕分けてみてください。窓に入れられる一回性の仕事が3つ見つかれば、この10日間は良い清算期間になります。1つも見つからなければ、それは積み残しのない健全な運用ということなので、枠の増加は気にせず普段どおりで良いはずです。同じように定期実行を整理しながら運用している方の参考になれば幸いです。