6月の更新でレート上限が引き上げられたと知って、最初に頭をよぎったのは「これで自動投稿の間隔をもっと詰められる」でした。私は個人開発で複数のサイトを一人で回していて、決まった時刻に API を叩く処理がいくつも並んでいます。上限に余裕ができたなら、これまで慎重に空けていた間隔を縮めて、同じ時間帯に詰め込めるはずだ、と。
結論から書きますと、間隔は詰めませんでした。代わりに私自身は、増えた余裕をそっくり「失敗したときの再試行枠」に回しました。半月ほど運用してみて、これが無人で回す処理にとっては正解だったと感じています。理由を順に書いていきます。
レート上限の余裕は「使い切る」ものではない
上限が上がると、つい天井いっぱいまで使う前提で設計したくなります。けれど自動運用で本当に困るのは、平常時のスループットではなく、何かが詰まった瞬間です。デプロイの切り替わり、一時的なネットワークの揺れ、参照先サービスの遅延。こうした瞬間に処理が重なると、上限がいくら高くても、そのとき同時に走っている本数がそのまま 429(レート超過)の引き金になります。
天井を高くした分を平常時の本数で埋めてしまうと、いざ揺れたときに吸収する余白がなくなります。私は「上限の余裕=同時実行を増やす許可」ではなく「揺れたときに再試行で押し返すための在庫」として扱うことにしました。
レート上限とクレジット消費は別の軸で考える
ここを混同すると設計を間違えます。レート上限は「単位時間あたりに何本叩けるか」というスループットの話で、超えると 429 が返ります。一方、月次のクレジット消費は「総量をいつ使い切るか」というコストの話です。前者は秒・分の問題、後者は日・月の問題で、効く対策がまったく違います。
クレジットの使いすぎを月初に集中させない配分は別の記事(繰越されない月次クレジットを月初に枯らさず月末に余らせない — バーンレート配分スケジューラの設計)で扱いました。この記事は純粋にスループット側、つまり 429 とどう付き合うかに絞ります。上限が倍になっても、429 に当たったときの振る舞いが雑なら、安定性はほとんど上がりません。
同時実行の本数は上限と切り離して固定する
私が最初にやったのは、スケジュールを「同じ瞬間に重ならない」ように並べ直すことでした。上限の数字には触れません。各タスクの開始時刻をずらし、ジッター(数分のランダムなずれ)を入れて、たまたま全部が同じ分に集中する事故を防ぎます。
上限を基準に「理論上は同時に N 本いける」と詰めるより、「同時に走るのは常に1〜2本」と決めてしまうほうが、無人運用では圧倒的に読みやすくなります。上限はそのための安全マージンであって、埋める対象ではない、という割り切りです。
増えた余裕は再試行とバックオフに回す
ここが本題です。空いたスループットの余裕を、429 や一時エラーに当たったときの指数バックオフ付き再試行に充てます。重要なのは、サーバーが返してくる retry-after を尊重することです。自分で決めた待ち時間より、サーバーの指示を優先します。
// 429 と一時エラーに対して、retry-after を尊重した指数バックオフで再試行する
async function callWithBackoff(doRequest, { maxRetries = 5, baseMs = 1000 } = {}) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await doRequest();
// 成功、または再試行しても無駄なクライアントエラーはそのまま返す
if (res.status !== 429 && res.status < 500) return res;
if (attempt === maxRetries) return res; // 使い切ったら最後の結果を返す
// サーバーが retry-after を返していれば最優先で従う(秒単位)
const retryAfter = res.headers.get("retry-after");
const serverWait = retryAfter ? Number(retryAfter) * 1000 : 0;
// 指数バックオフ + フルジッター(同時再試行の同期を崩す)
const backoff = baseMs * 2 ** attempt;
const jittered = Math.random() * backoff;
const waitMs = Math.max(serverWait, jittered);
await new Promise((r) => setTimeout(r, waitMs));
}
}
// 使い方の例
const res = await callWithBackoff(() =>
fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [{ role: "user", content: "..." }],
}),
})
);ポイントは2つです。ひとつは retry-after と自前のバックオフを比べて長いほうを採用していること。サーバーが「もっと待て」と言っているのに自分の計算で早く叩き直すと、また 429 を踏みます。もうひとつはフルジッターです。複数の処理が同時に 429 を踏むと、固定の待ち時間では再試行のタイミングまで揃ってしまい、同じ瞬間にまた殺到します。待ち時間を 0〜backoff の範囲でばらけさせることで、この同期を崩します。
上限が低かった頃は、再試行を入れると今度はその再試行が次の上限超過を呼ぶ悪循環がありました。上限に余裕ができた今は、この再試行が安心して回せます。これこそが、私が間隔を詰めずに余裕を残した理由です。
監視するのは「上限到達」ではなく「再試行回数」
最後に運用の話です。上限緩和後、私は Dolice Labs の運用ダッシュボードで見る指標を変えました。以前は 429 が出た回数そのものを見ていましたが、今は「1回の処理あたり何回再試行したか」を見ています。
429 はバックオフで吸収できるので、たまに出ること自体は問題になりません。危険なのは、再試行回数がじわじわ増えていくときです。それは平常時の本数が上限に近づいてきたサインで、上限が高くても「同時に走る本数が増えすぎている」可能性を示します。再試行回数が上がり始めたら、間隔を詰めるのではなく、むしろ開始時刻をさらにばらけさせる。これが半月運用して固まった私の判断基準です。
無人で動く処理を増やしていくと、上限の数字よりも「詰まったときに自分で回復できるか」のほうがずっと効いてきます。同じように複数の自動処理を抱えている方は、上限が上がった今こそ、間隔を詰める誘惑をいったん脇に置いて、再試行とジッターの設計を見直してみてください。次に上限が揺れたとき、その余裕が効いてきます。