Cloudflare Workers で定期実行を組もうとすると、無料プランの cron トリガー上限(アカウント単位で5本) に最初に当たります。1サービスで用途ごとに cron を切ると5本はすぐ埋まり、同じアカウントで別サービスも動かしたい、となると足りません。
対処はシンプルで、cron を毎分1本に集約し、scheduled() の中で時刻を見てジョブを振り分ける。これで6種類の定期処理を回しても、消費する cron は1本だけになります。個人で作った価格ウォッチャーでこの形にしたので、要点をまとめます。
cron は毎分1本、tick の中で時刻分岐
cron 式は * * * * *(毎分)1本だけ登録し、ハンドラの中で「今この分は何をやるか」を分岐します。
export const CRON_TICK = '* * * * *'; // wrangler.toml の crons と一致させる
export async function runScheduledTick(env: Env, now: Date): Promise<void> {
if (isPatrolTick(now)) await runPatrol(env, now); // 5分毎
if (isKaidokiDayTick(now)) await runKaidokiDay(env, now); // JST00:00
if (isHousekeepingTick(now)) await runHousekeeping(env, now); // JST03:30
await runDispatchCron(env, now); // 毎分(送信)
}
// Workers エントリ
export default {
async scheduled(_e: ScheduledController, env: Env, ctx: ExecutionContext) {
ctx.waitUntil(runScheduledTick(env, new Date()));
},
};
毎分の tick ごとに、その分に該当するジョブだけが走ります。用途を足しても cron は増えません。
分岐の述語は UTC で書く
scheduled に渡る時刻は UTC です。ローカルタイムのつもりで書くとズレるので、Date の UTC メソッドで書き、日本時間で動かしたいものは JST = UTC+9 で換算します。
export const isPatrolTick = (n: Date) => n.getUTCMinutes() % 5 === 0;
export const isKaidokiDayTick = (n: Date) => n.getUTCHours() === 15 && n.getUTCMinutes() === 0; // JST00:00
export const isHousekeepingTick = (n: Date) => n.getUTCHours() === 18 && n.getUTCMinutes() === 30; // JST03:30
述語を関数に切り出すと、runScheduledTick は「該当したら呼ぶ」を並べるだけで読めます。テストは new Date('2026-07-25T18:30:00Z') のような UTC 文字列を渡せば分岐を単体で確認できます。
信頼性は専用 cron と変わらない
「毎分1本にまとめると日次処理が不安定になるのでは」と思うかもしれませんが、依存先が同じなので信頼性は変わりません。
「UTC18:30 に発火する専用 cron」も「毎分 cron の UTC18:30 の発火で時刻判定する」も、どちらも Cloudflare がその分に cron を発火させること に等しく依存しています。発火が飛べば専用 cron も飛ぶし、毎分 tick が来ていれば時刻判定も成立します。集約で増える不確実性はありません。
運用で一点だけ効くのは、wrangler.toml の [triggers] crons とコードの cron 定数を 1文字違わず一致させる ことです。ズレると発火しない/余計に登録されるので、両者を突き合わせるテストを1本置くと安全です。
無料枠のサブリクエスト(50)にバッチを収める
もう一つの無料枠制限として、1回の実行あたりの外向き fetch(サブリクエスト)は50本までです。cron を集約しても、1 tick の処理がこれを超えると失敗します。だからジョブごとに「1件で何サブリクエスト使うか」からバッチサイズを逆算します。
// 送信: 1件=送信直前の再照会 + 配信 で2サブリクエスト → バッチは半分に
const batchSize = Math.floor(PUSH_BATCH_SIZE / 2);
await runDispatch(deps, batchSize, now);
巡回のように1件1リクエストなら40件程度に、送信のように1件2サブリクエストなら半分に。1回で捌き切れない取り込みは、対象をローテーションで数件ずつに割り、数日かけて全体を一周させます。50本の壁は「一度に全部やらない」で越えます。
同じ tick で投入したジョブを同じ実行で拾う呼び出し順序の設計や、重い日次処理を別時刻に散らす運用、この cron が回している通知配送(ログイン不要の Web Push)の中身は、Aulvem 本家にまとめました → Aulvem|Workers の cron 1本で複数ジョブを回す。動いているものはヤスゴロで触れます。