EmDash の Cron は Free プランだと5本まで
Cloudflare の同じアカウントで、Astro 製の CMS「EmDash」のサイトを9つ運用しています。すべてのサイトに Cron Trigger(Workers を決まった時刻に起動する Cloudflare の仕組み。設定は wrangler.jsonc の triggers.crons に書く)を1日1回で足してデプロイしたところ、6サイトで登録だけが失敗しました。原因は Workers Free の Cron Trigger がサイトごとではなくアカウント全体で5本しかないことで、上限を知らずに足していました。
EmDash は Cron Trigger が起動するたびに呼ばれる関数(scheduled())で、予約公開や期限切れデータの掃除を回す前提で作られています。Cron を置くサイトを2つに絞り、残りのサイトは設定に空の配列を書いて Cron を消すことで、枠に収めました。
Cron の登録だけが失敗し、Worker は 200 を返し続けた
9サイトに1日1回の Cron を足してデプロイすると、6サイトで次のエラーになりました。
✘ [ERROR] Trigger configuration for "<サイト>" was only partially updated:
Cron schedules:
- A request to the Cloudflare API (/accounts/<省略>/workers/scripts/<サイト>/schedules) failed.
- This account has reached the Workers Free limit of 5 cron triggers per account.
Successful trigger changes were not rolled back.先にデプロイされた3サイトが枠を取っていたため、残り6サイトの登録がここで弾かれました。このとき Worker 本体のアップロード自体は成功していて、サイトは 200 を返し続けています。失敗するのは Cron の登録だけで、デプロイのジョブだけが赤くなります。設定を戻さない限り、そのサイトは次のデプロイでも毎回同じ場所で落ちます。
EmDash は全サイトに Cron Trigger がある前提で定期処理を組んでいる
EmDash の Cloudflare 用 Worker は scheduled() を export しており、Cron Trigger が起動するたびに予約公開、プラグインの定期タスク、期限切れトークンと放棄されたアップロードの掃除、古いリビジョンの間引き、有効時は23時間おきのバックアップをまとめて回します。emdash doctor(構成の健全性を確認する CLI コマンド)も、この関数が export されているのに Cron Trigger が無いサイトを見つけると、次のように毎分の Cron を足すよう指示します。
// packages/core/src/cli/commands/doctor.ts (L187-L195)
if (hasScheduledHandler && !hasCronTrigger) {
return [
{
name: "scheduler trigger",
status: "fail",
message: `EmDash scheduled() handler is exported, but no Cron Trigger is configured — add this to ${configPath}: ${triggerFix(configPath)}`,
},
];
}このチェックは doctor.ts の187〜195行目にあります。差し込まれる値 "triggers": { "crons": ["* * * * *"] }(毎分)は、31行目で定義されています。
Cron Trigger を持たないサイトでは、この掃除が動きません。実際、Cron の無かった shinobiworks に残っていたのは、期限切れトークン3件、パスキー(生体認証などでパスワード無しにログインする仕組み)のログイン用一時データ1件、間引き待ちのリビジョン5件、途中で止まったアップロード2件でした。表示や管理画面は壊れません。パスキーの一時データだけはログイン処理そのものでも消えますし、期限切れのトークンは使うたびに期限を確認しているので、残っていても使えません。
Cron Trigger はアカウント全体で5本という共有の枠だった
Cloudflare の制限表は、Workers Free の Cron Trigger をアカウントあたり5本と定めています。サイトごとの枠ではなく、同じアカウントで動く Worker 全部で分け合う枠です。このアカウントでは EmDash のサイト9つに加えて、監視用の Worker が Cron を2本使っています。9サイト分を単純に足すと11本になり、5本の枠からあふれます。
Cloudflare のドキュメントの通り、triggers や crons の項目そのものを書かないとデプロイ済みの Cron Trigger は消えず残り続け、"crons": [] と空の配列を明示して初めて消えます。
置くサイトと頻度は問い合わせと投稿の有無で決めた
Cron を置くサイトは2つに絞りました。基準は、消すべきデータが実際に溜まること(ログインと記事の投入がある)と、問い合わせフォームを持つことです。稼働していないサイトには置きません。監視用の2本と合わせて、5本の枠のうち4本を使っています。Cron を置かないサイトには "crons": [] を明示し、デプロイ済みの Cron Trigger を消しました。
頻度を上げると tableExists の全件走査も比例して増える
Cron を置くサイトを絞っても、頻度は別のコストになります。EmDash は予約公開の前に、画像の使用状況を記録する機能(media usage)の初期化中に書き込みを止めるための確認を挟みます。
予約公開の処理(emdash-runtime.ts の742〜745行目)は、最初にこの確認(media-usage-write-fence.ts の38〜41行目)を呼び、確認は tableExists で記録用のテーブルがあるかを調べます。
tableExists は SQLite の場合、sqlite_master(テーブルやインデックス、トリガーの定義を SQLite が1件1行で持つ内部の表)に次のクエリを投げます。
// packages/core/src/database/dialect-helpers.ts (L140-L145)
const result = await sql<{ name: string }>`
SELECT name FROM sqlite_master
WHERE type = 'table' AND name = ${tableName}
`.execute(db);
return result.rows.length > 0;このクエリは dialect-helpers.ts の140〜145行目にあります。
sqlite_master は絞り込みに効く索引を持たず、この確認1回で全行を読みます。対象のサイトでは314行(インデックス234、テーブル71、トリガー9)で、1回の確認がそのまま314 rows read になる計算です(D1 の課金の単位である rows read は、返した行ではなく読んだ行を数える)。
emdash doctor の推奨どおり毎分の Cron を持っていたサイトでは、この確認が1日約1,400回、約43万行に達していました。Workers Free の D1 は読み取りがアカウント合計で1日500万行までなので、この確認だけで日次の枠の約9%を使う計算です。最新の0.38.0でもこの確認は残ったままです。
Cron を1日1回(0 18 * * *、日本時間3時)にしたことで、この確認によるコストは1サイトあたり1日314行まで下がる計算です。確認は予約公開する記事の有無に関係なく Cron が動くたびに走るので、回数は Cron の頻度だけで決まります。
Cron Trigger の5本は動かせない、動かせるのは置き方と頻度
- Cron を置かないサイトは、掃除が動かないまま期限切れデータが溜まり続ける。表示や機能は壊れないが、無制限に溜め続けてよいという確認はしていない
- Cron を1日1回にしたことで、掃除の対象(数分〜数日で期限が切れるデータ)が最大1日分遅れて消える。予約公開を使っていないので、公開のタイミングには影響しない
- Cron を1日1回にしたあとの D1 の実測はまだ取っていない
Cron Trigger の枠はアカウント単位の5本で固定です。今回変えたのは、そのうちどのサイトに何本を、どの頻度で割り当てるかでした。
参考リンク
- Workers の制限表(Cron Trigger はアカウントあたり5本)
- Cron Trigger の設定と削除(空の配列と未設定の違い)
- D1 の料金と rows read の定義
- EmDash のソース: `scheduled()` の Cron Trigger 診断(doctor.ts)
- EmDash のソース: 予約公開のフェンス処理(emdash-runtime.ts)
- EmDash のソース: media usage の書き込みフェンス(media-usage-write-fence.ts)
- EmDash のソース: `tableExists` の実装(dialect-helpers.ts)