おはようございます。まさきんです。
Cloudflare Workers の Cron Triggers は、公式ドキュメントで無料プランの上限が5本と決まっています。僕も家の自動化を無料枠でやりくりしていて、先日この上限に当たりました。そのときの逃げ方を、公式の情報と合わせて整理しておこうと思います。
※記事公開時点の情報です。最新の条件は公式サイトでご確認ください。
- Workers Freeでは、Cron Triggersはアカウント全体で5本までです
- 近い用途の時刻は1本の式にまとめ、ハンドラ内で振り分けられます
- 起動役だけを自宅PCなどに出せば、処理はWorkerに残せます
- 外から叩くなら、秘密トークンでの認証とタイムアウトの設定が要ります
- それでも足りなければ、有料プランの250本に移るのが素直です
上限に当たった経緯
健康データを通知するWorkerに、Cron Triggersを3本足してデプロイしました。すると、スケジュールの設定だけが失敗しました。エラーコードは10072で、Workers Freeのアカウント上限に当たっていたのが原因です。
コード自体のデプロイは通っていました。なので、一見すると成功したように見えるのが少しやっかいです。ログを見ずに放っておくと「なぜか動かない」になっていたかもしれません。
無料枠の上限は5本
公式のLimitsページでは、Cron Triggersの本数はアカウント単位で決まっています。Worker 1本ごとではない点が大事です。
| 項目 | Workers Free | Workers Paid |
|---|---|---|
| Cron Triggersの本数(アカウント全体) | 5 | 250 |
| Cron起動時のCPU時間 | 10ms | 30秒(1時間未満の間隔)/15分(1時間以上の間隔) |
| HTTP起動時のCPU時間 | 10ms | 最大5分(既定30秒) |
| リクエスト数 | 1日10万まで(UTC 0時にリセット) | 月1,000万まで料金に込み |
家の自動化は、小さなWorkerを用途ごとに分けがちです。1本のWorkerに2〜3個のスケジュールを持たせると、5本はすぐに埋まります。僕の場合も、気づいたら満杯でした。
Cron Triggersの基本をおさらい
逃げ方を考える前に、仕様を確認しておきます。ここを押さえると、打てる手がいろいろ見えてきます。
- 式は5フィールド(分・時・日・月・曜日)で書く
- 時刻はUTCで評価される
- 1つのWorkerに複数のスケジュールを配列で設定できる
- scheduledハンドラでは、どの式で起動したかを
controller.cronで判別できる - 追加や変更の反映には、最大15分ほどかかることがある
注意したいのはUTCの点です。日本時間はUTCより9時間進んでいます。朝6時30分に動かしたいなら、UTCでは前日の21時30分と書くことになります。
controller.cronには、設定した式の文字列がそのまま入ります。公式では、スペースも含めて一字一句一致させる必要があると書かれています。古い書き方のevent.cronで書いたコードも、考え方は同じですね。
逃げ方は大きく4つ
僕が検討した選択肢を、先に表で並べておきます。
| 逃げ方 | 枠の節約 | 長所 | 短所 |
|---|---|---|---|
| (1) 複数の時刻を1本の式にまとめる | 大 | 追加の仕組みが要らない | 時刻の振り分けをコードで持つ |
| (2) 起動役だけ外に出す | 大 | 頻度を自由に増やせる | 外の起動役が止まると動かない |
| (3) ディスパッチャWorkerに集約する | 大 | Cloudflareの中で完結する | 1か所の不具合が全部に響く |
| (4) 有料プランへ移る | 根本解決 | 上限が250本に広がる | 月額5ドルから費用がかかる |
(1)から試して、足りなければ(2)か(3)に進む。最後に(4)を考える。この順番が、無料枠でやりくりするなら素直かもしれません。
逃げ方1:時刻を1本の式にまとめる
一番手軽なのがこれです。朝と昼に別々の式を書いていたとします。同じWorkerの中なら、時の欄をカンマでつなげば1本になります。
僕は朝と昼の2本を、次の1本にまとめて再デプロイしました。
[triggers]
crons = ["30 3,21 * * *"]
UTCの21時30分は、日本時間の朝6時30分です。UTCの3時30分は、日本時間の昼12時30分です。これで枠が1本空きました。
式が1本になると、controller.cronではどちらの時刻か区別できません。なので、起動予定の時刻からUTCの「時」を取り出して振り分けます。
export default {
async scheduled(controller, env, ctx) {
const hourUtc = new Date(controller.scheduledTime).getUTCHours();
if (hourUtc === 21) {
await sendMorning(env); // 日本時間 6:30
} else if (hourUtc === 3) {
await sendNoon(env); // 日本時間 12:30
}
},
};
scheduledTimeは、起動が予定されていた時刻です。実際に動いた時刻より、こちらで判定するほうが安定すると思います。
ただ、まとめられるのは「分」が同じ時刻に限られます。6時30分と12時15分のように分がずれると、1本の式では表せません。そのときは、分をそろえてよいか一度考えてみるのがよさそうです。
まとめても満杯なら
僕の場合、朝昼をまとめても5本は満杯のままでした。新しい処理を足す余地がありません。
そこで、残りのCronを見直しました。位置情報の同期が1本、家のサイネージ更新が2本です。この3本は、どれも自宅のMacがある前提の処理でした。なので、起動役を外に出す手を選びました。
家の中の自動化は、結局ネット越しの通信で動いています。回線まわりを整えておくと、こういう仕組みも安心して回せる気がします。
逃げ方2:起動役だけ外に出す
考え方はシンプルです。処理はWorkerに残したまま、「いつ動かすか」だけを外に任せます。外の起動役は、WorkerのURLをHTTPで叩くだけです。
僕は位置情報の同期とサイネージ更新のCronを外しました。代わりに、自宅のMacのlaunchdからWorkerのURLを叩く形に移しています。これで空き枠は3本になりました。
うれしい副作用もありました。位置情報の同期は1日1回だったのが、朝8時05分から夜20時05分まで毎時に増やせました。Cronの枠を気にしなくてよいのが大きいですね。
Worker側に受け口を作る
Worker側には、起動用のパスを用意します。大事なのは、合言葉を知っている相手だけを通すことです。
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname !== "/run/location-sync") {
return new Response("not found", { status: 404 });
}
const auth = request.headers.get("Authorization");
if (auth !== `Bearer ${env.RUN_TOKEN}`) {
return new Response("unauthorized", { status: 401 });
}
await syncLocation(env);
return new Response("ok");
},
};
トークンはコードに直接書かず、シークレットとして登録します。登録にはnpx wrangler secret put RUN_TOKENを使います。コードからはenv.RUN_TOKENで読めます。
Mac側はlaunchdで叩く
Mac側では、launchdのジョブからcurlを実行します。流れは次のとおりです。
- トークンを読み込み、curlでWorkerのURLを叩くシェルスクリプトを作る
~/Library/LaunchAgentsにplistを置き、実行時刻を指定するlaunchctlでジョブを読み込む- 手動で一度実行して、Worker側のログで成功を確かめる
スクリプトの中身は、ほぼcurlの1行です。
curl -fsS --max-time 600 \
-H "Authorization: Bearer ${RUN_TOKEN}" \
https://example.workers.dev/run/location-sync
launchdはMacのローカル時刻で動きます。Cron TriggersのようにUTCへ直す必要がないのは、地味に楽なところです。
外から叩くときの注意点
外から起動する形には、Cron Triggersにはない注意点があります。僕が実際に引っかかった点も含めて並べておきます。
URLを知っていれば誰でも叩けてしまいます。秘密トークンで認証し、トークンはシークレットに置いてください。curlのタイムアウトは、処理時間より長めに取るのが安全です。
一番ハマったのはタイムアウトです。Macから叩く処理は、2日分をまとめると約73秒かかりました。curlのタイムアウトを90秒にしていたところ、時間切れになりました。今は600秒に延ばしています。
公式によると、HTTPで起動したWorkerの実行時間に硬い上限はありません。ただ、クライアントが接続している間だけという条件付きです。切断されると、処理が途中で止まる可能性があります。なので、呼ぶ側のタイムアウトは余裕を持たせたほうがよいと思います。
もう1つはCPU時間です。Freeでは1回あたり10msまでです。ただ、fetch()やデータベースの応答待ちはCPU時間に数えられません。外部APIを待つだけの処理なら、長く見えても収まることが多いかもしれません。
逃げ方3:ディスパッチャWorkerに集約する
複数のWorkerがそれぞれCronを持っているなら、集約する手もあります。1本の「ディスパッチャ」Workerだけにスケジュールを持たせる形です。
ディスパッチャはcontroller.cronや起動時刻を見て、担当のWorkerを呼び出します。呼び出し先へは、HTTPで叩く形が分かりやすいと思います。
この方法の良い点は、Cloudflareの中で完結することです。自宅のPCが寝ていても動きます。ただ、ディスパッチャが壊れると全部の定期処理が止まります。Freeではサブリクエストが1回あたり50までなので、呼び出し先が多いときは気にしておきたいところです。
逃げ方4:有料プランに移る
いちばん素直なのは、Workers Paidに移ることです。料金ページでは、アカウントあたり月額5ドルからとされています。
上限は5本から250本に広がります。Cron起動時のCPU時間も伸びるので、重めの処理も置きやすくなります。
個人的には、家の自動化で250本も使うことはなさそうです。枠の問題だけなら、まず(1)〜(3)で様子を見る。処理の重さで困ったら有料を考える。そんな順番でよいのかなと思います。
僕が受け入れたトレードオフ
起動役をMacに移したことで、はっきり失ったものがあります。サイネージはMacが動いていないと更新されなくなりました。Macを経由しないで届く経路は、もうありません。
とはいえ、サイネージを見るのは家にいるときです。家にいるときはMacも動いていることが多いです。この前提なら受け入れられる、と判断しました。
逆に、外出中も確実に届いてほしい通知はCron Triggersに残すほうが安心だと思います。「止まっても困らないもの」から外に出していく、という線引きが大事だと感じています。
よくある失敗・落とし穴
実際にやってみて、ここは気をつけたいと思った点です。
- UTCで書くのを忘れて、9時間ずれた時刻に動く
- 式を1本にまとめたのに、
controller.cronの判定を古い式のまま残す - デプロイは成功しても、スケジュールの設定だけ失敗しているのに気づかない
- 起動用URLに認証を付けず、誰でも叩ける状態にしてしまう
- curlのタイムアウトが短く、処理の途中で切断される
- 自宅PCがスリープ中で、起動役が動かないことを想定していない
特に3つ目は見落としがちです。デプロイ後は、ダッシュボードかWranglerの出力でスケジュールが登録されたか確認しておくと安心です。
向いている人・向いていない人
逃げ方2(起動役を外に出す)は、向き不向きがはっきりしています。
| 内容 | |
|---|---|
| 向いている人 | 常時動いている自宅PCやサーバーがある / 家にいるときだけ動けばよい処理がある / 頻度を細かく増やしたい |
| 向いていない人 | 外出中や旅行中も確実に動いてほしい / 自宅PCを頻繁にスリープさせる / 認証まわりを管理したくない |
向いていない側なら、(1)か(3)のほうが合っているかもしれません。有料プランも選択肢です。
始める前に確認すること
- いまアカウント全体で何本のCron Triggersを使っているか数えた
- それぞれの式をUTCで書いているか確認した
- 同じWorkerの中で分がそろっていてまとめられる式がないか見た
- 外に出す候補の処理がPCが止まっても困らないものか考えた
- 起動用のパスに秘密トークンの認証を付ける準備をした
- 呼ぶ側のタイムアウトを処理時間より長く取った
- デプロイ後にスケジュールが登録されたかを確認する手順を決めた
よくある質問
Q. 5本の上限はWorkerごとですか?
アカウント全体で5本です。Worker 1本ごとに5本ではありません。複数のWorkerに分けても、合計で数えられます。
Q. 上限を超えるとデプロイ自体が失敗しますか?
僕の場合は、コードのデプロイは通りました。失敗したのはスケジュールの設定だけです。エラーコードは10072でした。一見成功に見えるので、出力はよく確認したほうがよいと思います。
Q. 1本の式に何個の時刻まで入れられますか?
時の欄をカンマでつなげば、分が同じ時刻をいくつでもまとめられます。ただ、分が違う時刻は同じ式では表せません。まとめすぎると、ハンドラ内の振り分けが読みにくくなる点も気をつけたいところです。
Q. controller.cronとevent.cronは違いますか?
今の公式ドキュメントではcontroller.cronで判別する形です。controllerはscheduledハンドラの第1引数です。古いコードではevent.cronと書かれていることがあります。どちらも、起動した式の文字列を見る考え方は同じです。
Q. 外から叩く場合、無料枠のリクエスト数は大丈夫ですか?
Freeでは1日10万リクエストまでです。毎時1回叩く程度なら、1日十数回なので余裕があります。ただ、他の用途と合算される点は覚えておくとよいかもしれません。
Q. 自宅PC以外に起動役を任せる方法はありますか?
外部のスケジュール実行サービスや、別のクラウドの定期実行機能から叩く方法もあります。その場合も、認証とタイムアウトの考え方は同じです。どのサービスを使うかは、止まったときの影響で選ぶのがよいと思います。
Q. 有料プランにすればCPU時間の心配もなくなりますか?
Cron起動時のCPU時間は、Paidでは間隔に応じて30秒か15分まで伸びます。Freeの10msよりかなり余裕があります。ただ、上限そのものが無くなるわけではない点には注意が必要です。
参考にした公式情報
- Limits · Cloudflare Workers docs
- Cron Triggers · Cloudflare Workers docs
- Scheduled Handler · Cloudflare Workers docs
- Pricing · Cloudflare Workers docs
- Secrets · Cloudflare Workers docs
まとめ
Cron Triggersの上限は、アカウント全体で5本です。家の自動化を続けていると、意外と早く当たります。
僕は、まず朝昼の式を1本にまとめました。次に、Macがある前提の処理だけ起動役を外に出しました。おかげで枠に余裕ができ、位置情報の同期も毎時に増やせています。
その代わり、サイネージはMac頼みになりました。どこまでを外に出すかは、止まったときに困るかどうかで決めるのがよさそうです。
ちなみに、家のサイネージの中身については学校の連絡帳をAIに読ませて家のディスプレイに反映する話に書いています。朝の情報を1枚にまとめる仕組みは朝のブリーフィングを自動生成している話にまとめました。あわせて読んでみてください。
自動化の足回りを見直すついでに、毎月の通信費も一度眺めてみるのはいかがでしょうか。
本記事にはアフィリエイト広告を含みます。当サイトのリンクを経由して商品・サービスへお申し込みいただいた場合、提携先企業から成果報酬を受け取ることがあります。 また、運営者は楽天グループに所属する社員であり、紹介プログラムを通じた報酬を受け取る場合があります。記事内容・評価は広告の有無にかかわらず、運営者の実体験と調査に基づき作成していますが、 上記の関係性をご理解のうえお読みください。詳しくは免責事項・アフィリエイト表記をご覧ください。
楽天モバイルの通信エリア・電波状況について気になることがある方は、公式の電波改善・調査依頼フォームからご相談いただけます。