+ 作るログイン
図を読み込んでいます…
1 / 14
Cloudflare のすべて ― 1機能ずつ、仕組みと存在理由から / 全35本

Cloudflare 25:Workflows と Cron Triggers ― 最後までやり遂げる、決まった時刻に動かす

発行
アニメモ公式
版
第1版
改訂
コマ
全14

何段もある処理を確実に最後まで実行する Workflows と、決まった時刻に Worker を動かす Cron Triggers を説明します。段ごとに結果を記録する仕組み、step.do・step.sleep・step.waitForEvent、段ごとの再挑戦、始め方、cron 式と UTC、料金と制限、落とし穴、Queues との使い分けまで。

何段もある処理を、1回のリクエストでやっている

ネットショップで注文が入ったときの処理を考えます。

段やること
1注文の中身と在庫を確かめる
2決済サービスで支払いを確定する
33日待つ
4「商品はいかがでしたか」とレビュー依頼のメールを送る

これを1回のリクエストの中で書こうとすると、すぐに行き詰まります。まず、3日待つことができません。Worker の1回の実行は、長くても数分で終わる前提の仕組みです。

もっと困るのは、途中で落ちたときです。2段目の支払いまで済んで、3段目で何かが失敗したとします。どこまで済んだのかが分からないので、最初からやり直すしかありません。すると、支払いが二重になります。

これまでは、「どこまで進んだか」をデータベースに書き込み、定期的に見回って続きを動かす仕組みを、自分で作る必要がありました。手間がかかるうえに、バグの温床です。

この号では、こうした何段もある処理を確実に最後までやり遂げる Workflows と、決まった時刻に Worker を動かす Cron Triggers を見ます。

※この号の内容は2026年9月時点の情報です。料金・上限・提供状況は変わることがあるので、使う前に必ず公式のドキュメントで確かめてください。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 1 / 14
  • 何段もある処理を、1回のリクエストでやっている
  • 注文を確認
  • 支払いを確定
  • 3日待つ
  • レビュー依頼
  • ここで落ちた
  • 注文 → 支払い → 3日後にメール。Worker 1回の実行では「3日待つ」ができない
  • 途中で落ちると、どこまで済んだか分からない。やり直すと支払いが二重に
  • 3段目で落ちたら? 最初からやり直すと、支払いが二重になる。

Workflows:段ごとに結果を記録しながら進む

Workflows は、何段もある処理を、途中で失敗しても確実に最後まで実行するための仕組みです。こうした仕組みを一般に永続実行(durable execution)と呼びます。2025年に一般提供(GA)になりました。

考え方はシンプルです。処理を段(ステップ)に分けて書きます。1つの段が終わるたびに、その段の結果を Cloudflare が保存します。図の下にある「状態の記録」がそれです。

途中で何かが起きて止まっても、再開するときは記録を見ます。記録がある段は、もう一度実行せず、記録から結果を取り出すだけにします。記録の無い段から、続きを実行します。

よくある困りごとWorkflows では
途中で落ちた記録のある段は飛ばして、続きから再開する
一時的な失敗その段だけを、決めた回数やり直す
長く待ちたい眠って待てる。3日でも、1か月でも
人の承認を待ちたいイベントが届くまで待てる

Workflow を1回起動したものをインスタンスと呼びます。注文1件ごとに1つのインスタンスを起動する、という使い方をします。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 2 / 14
  • Workflows:段ごとに結果を記録しながら進む
  • いまここ
  • 注文を確認
  • 支払いを確定
  • 3日待つ
  • レビュー依頼
  • 状態の記録(Cloudflare が保存する。落ちても消えない)
  • Workflows = 何段もの処理を「確実に最後まで」実行する仕組み(永続実行)
  • 段(ステップ)が終わるたびに、その結果を Cloudflare が保存する
  • 落ちても、記録のある段は飛ばして続きから再開できる。

step.do:1段ずつ実行し、結果を保存する

段を書くいちばん基本の道具が step.do() です。

ts
const order = await step.do("注文を確認", async () => {
  const res = await fetch(`https://api.example.com/orders/${event.payload.id}`);
  return await res.json(); // この値が記録される
});

1つめの引数は段の名前、2つめが実際に行う処理です。処理が返した値が、状態の記録に保存されます。次の段では、その値(ここでは order)をふつうの変数として使えます。

段の名前は、記録の目印になります。再開したとき、Workflows は「この名前の段は、もう記録がある」と判断して、処理を実行せずに記録の値を返します。だから、段の名前は毎回同じになるように付けます。日時や乱数を名前に入れてはいけません。

段の中で返す値は、JSON にできる値にします。大きなデータ(画像や大量の行)をそのまま返すと記録の上限に当たるので、R2(22号)や D1(23号)に置いて、段はそのキーだけを返すのが定番です。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 3 / 14
  • step.do:1段ずつ実行し、結果を保存する
  • いまここ
  • 注文を確認
  • 支払いを確定
  • 3日待つ
  • レビュー依頼
  • 状態の記録(Cloudflare が保存する。落ちても消えない)
  • 注文 #42 OK
  • step.do("名前", 関数) で1段。関数が返した値が記録される
  • 次の段は、その値を受け取って続ける。段の名前が記録の目印になる
  • 終わった段の結果は記録に残る。次の段はそれを受け取る。

段が失敗したら、その段だけをやり直す

段の中の処理が例外を投げると、Workflows はその段だけをやり直します。前の段の処理は、もう一度は実行されません。

ts
await step.do(
  "支払いを確定",
  { retries: { limit: 5, delay: "10 seconds", backoff: "exponential" }, timeout: "5 minutes" },
  async () => {
    await chargePayment(order);
  },
);

retries で、やり直す回数と間隔を決めます。backoff: "exponential" にすると、10秒、20秒、40秒…と、だんだん間隔が長くなります。相手のサービスが混んでいるときに、すぐに何度も叩かないためです。timeout は、1回の挑戦にかけてよい時間です。

回数を使い切っても失敗したら、そのインスタンスは「失敗」の状態で止まります。管理画面や wrangler で、どの段で止まったかを確かめられます。

大事なのは、Worker そのものが落ちても同じだということです。Cloudflare の拠点で障害が起きても、記録は残っているので、別の場所で「2段目の途中」として再開されます。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 4 / 14
  • 段が失敗したら、その段だけをやり直す
  • いまここ
  • 少し待って再挑戦
  • 注文を確認
  • 支払いを確定
  • 3日待つ
  • レビュー依頼
  • 状態の記録(Cloudflare が保存する。落ちても消えない)
  • 注文 #42 OK
  • 段ごとに再挑戦の回数と間隔(だんだん長く、など)を決められる
  • Worker ごと落ちても、記録から「2段目の途中」として再開される
  • 前の段はやり直さない。記録から結果を取り出して続ける。

step.sleep:3日でも、眠って待てる

「3日後にメールを送る」を、Workflows ではそのまま書けます。

ts
await step.sleep("3日待つ", "3 days");

// 決まった日時まで待つこともできる
await step.sleepUntil("発売日まで待つ", new Date("2026-12-01T00:00:00Z"));

眠っている間、コードは動いていません。どこかのサーバーが3日間このインスタンスのために待機しているわけではなく、「3日後に起こす」という記録が残っているだけです。時間が来ると、Workflows が続きから再開します。

これが、ふつうの setTimeout との決定的な違いです。

待ち方3日待てるか待っている間に落ちたら
setTimeoutWorker の実行時間の上限に当たる待ちごと消える
step.sleep待てる記録から再開される

眠っているインスタンスは、同時に動いているインスタンスの数にも数えられません。何万件もの注文が「3日待ち」の状態で眠っていても、動いている数の上限を圧迫しないということです。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 5 / 14
  • step.sleep:3日でも、眠って待てる
  • いまここ
  • 眠っている間も記録は残る
  • 注文を確認
  • 支払いを確定
  • 3日待つ
  • レビュー依頼
  • 状態の記録(Cloudflare が保存する。落ちても消えない)
  • 注文 #42 OK
  • 支払い済み
  • step.sleep("名前", "3 days")。決まった時刻まで待つ step.sleepUntil もある
  • 眠っている間は動いていないので、同時に動く数にも数えられない
  • 待っている間はコードが動いていない。サーバーを占有しない。

step.waitForEvent:人の承認や外の知らせを待つ

処理の途中で、外からの知らせを待ちたいことがあります。上司の承認、決済サービスからの入金通知、利用者のメール認証などです。これを書くのが step.waitForEvent() です。

ts
const approval = await step.waitForEvent("上司の承認を待つ", {
  type: "approval",
  timeout: "7 days",
});
// approval.payload に、知らせと一緒に送られた中身が入っている

知らせを送る側(たとえば承認ボタンの API を持つ Worker)は、インスタンスの ID でインスタンスを取り出し、イベントを送ります。

ts
const instance = await env.ORDER_FLOW.get(instanceId);
await instance.sendEvent({ type: "approval", payload: { approvedBy: "sato" } });

timeout までに知らせが来なければ、その段は失敗として扱われます。「7日たっても承認されなければ、注文を取り消す」のような処理も書けます。

待っている間は、眠っているときと同じく、コードは動いていません。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 6 / 14
  • step.waitForEvent:人の承認や外の知らせを待つ
  • いまここ
  • 注文を確認
  • 支払いを確定
  • 3日待つ
  • レビュー依頼
  • 状態の記録(Cloudflare が保存する。落ちても消えない)
  • 注文 #42 OK
  • 支払い済み
  • 承認された(イベント)
  • 外から sendEvent() で知らせると、待っていた段が起きる。期限も付けられる
  • 「承認ボタンが押されたら続ける」が、そのまま書ける。

コード:WorkflowEntrypoint の run() に段を並べる

Workflow は、WorkflowEntrypoint を継承したクラスとして書きます。run() の中に、段を上から順に並べます。

ts
import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers";

type Params = { id: string };

export class OrderFlow extends WorkflowEntrypoint<Env, Params> {
  async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
    const order = await step.do("注文を確認", async () => loadOrder(this.env, event.payload.id));

    await step.do("支払いを確定", { retries: { limit: 5, delay: "10 seconds" } }, async () => {
      await charge(order);
    });

    await step.sleep("3日待つ", "3 days");

    await step.do("レビュー依頼", async () => {
      await sendReviewMail(order);
    });
  }
}

ふつうの async 関数と同じように書けるのが Workflows の良さです。if で分岐したり、for で回したりもできます。

ひとつだけ強く意識してほしいのは、段の外に副作用を書かないことです。再開するたびに run() は頭から実行され直し、記録のある段は結果を返すだけになります。段の外に書いたメール送信やデータベースの書き込みは、再開のたびに何度も走ります。外に影響する処理は、必ず step.do の中に入れます。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 7 / 14
  • コード:WorkflowEntrypoint の run() に段を並べる
  • export class OrderFlow extends WorkflowEntrypoint<Env, Params> {
  • async run(event, step) {
  • const order = await step.do("注文を確認", () => loadOrder(event.payload.id));
  • await step.do("支払いを確定", { retries: { limit: 5, delay: "10 seconds" } }, () => charge(order));
  • await step.sleep("3日待つ", "3 days");
  • await step.do("レビュー依頼", () => sendReviewMail(order));
  • }
  • }
  • 段の外に副作用(送信・書き込み)を書かない。再開のたびに段の外は何度も走る
  • ふつうの async 関数のように上から書く。段は step で包む。

始め方:設定に書き、Worker から create() で起動する

Workflow を使うには、設定ファイルに Workflow を登録します。

jsonc
{
  "workflows": [
    { "name": "order-flow", "binding": "ORDER_FLOW", "class_name": "OrderFlow" }
  ]
}

class_name はコードのクラス名、binding はコードから呼ぶときの名前です。起動は、ふつうの Worker の fetch() などから行います。

ts
const instance = await env.ORDER_FLOW.create({ params: { id: orderId } });
return Response.json({ instanceId: instance.id });

create() は、インスタンスを起動したらすぐ返ります。Workflow の完了は待ちません。返ってきた ID を保存しておけば、後で状態を確かめたり、イベントを送ったりできます。

操作コマンド・API
デプロイnpx wrangler deploy
一覧npx wrangler workflows list
インスタンスの状態npx wrangler workflows instances describe
コードから状態を見る(await env.ORDER_FLOW.get(id)).status()

管理画面でも、インスタンスごとに「どの段まで進んだか」「どの段で何回失敗したか」が見られます。Workflows は Workers の無料プランでも試せます。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 8 / 14
  • 始め方:設定に書き、Worker から create() で起動する
  • // wrangler.jsonc
  • // fetch() の中で起動する
  • "workflows": [{
  • const inst = await env.ORDER_FLOW
  • "name": "order-flow",
  • .create({ params: { id } });
  • "binding": "ORDER_FLOW",
  • return Response.json({ id: inst.id });
  • "class_name": "OrderFlow"
  • }]
  • wrangler workflows list
  • 作った Workflow の一覧
  • wrangler workflows instances
  • インスタンスの状態・どの段か
  • 1回の起動=1つのインスタンス。ID で状態を確かめられる。

Cron Triggers:決まった時刻に Worker を動かす

ここからは、もう1つの機能 Cron Triggers です。これは、決まった時刻に Worker を動かす仕組みです。

Worker は、ふつうはリクエストが来たときに動きます。でも、「毎晩0時に売上を集計する」「1時間ごとに古いデータを消す」「15分ごとに外のサービスが生きているか確かめる」のように、誰も呼ばなくても、時間になったら動いてほしい仕事があります。

Cron Triggers を設定すると、Cloudflare の時計が決まった時刻に Worker の scheduled() ハンドラを呼びます。

ts
export default {
  async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
    if (controller.cron === "0 0 * * *") {
      ctx.waitUntil(aggregateDailySales(env));
    }
    if (controller.cron === "*/15 * * * *") {
      ctx.waitUntil(checkHealth(env));
    }
  },
};

1つの Worker に複数の時刻を設定できます。どの時刻で呼ばれたかは controller.cron で分かります。

Cron Triggers は、Workflows とも相性が良い組み合わせです。重くて何段もある夜間処理は、scheduled() の中で Workflow を create() するだけにして、実際の処理は Workflow に任せます。すると、夜間処理にも段ごとの記録と再挑戦が効くようになります。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 9 / 14
  • Cron Triggers:決まった時刻に Worker を動かす
  • 時刻が来たら
  • Worker の scheduled()
  • 集計・掃除・通知
  • 時計は UTC(世界標準時)
  • リクエストが来なくても、時計が Worker の scheduled() を呼ぶ
  • 毎晩の集計・古いデータの掃除・定期的な死活確認・Workflow の起動に使う
  • 毎日0時の集計、15分ごとの確認。誰も呼ばなくても動く。

書き方:cron 式は UTC。日本時間から9時間引く

時刻は、設定ファイルに cron 式で書きます。

jsonc
{
  "triggers": {
    "crons": ["0 0 * * *", "*/15 * * * *"]
  }
}

cron 式は、空白で区切った5つの欄でできています。

欄分時日月曜日
0 0 * * *00毎日毎月毎曜日
*/15 * * * *15分ごと毎時毎日毎月毎曜日
0 15 * * 1015毎日毎月月曜

いちばん大事なのは、時刻が UTC(世界標準時)で解釈されることです。タイムゾーンは指定できません。日本時間は UTC より9時間進んでいるので、日本時間から9時間引いて書きます。

やりたいこと(日本時間)cron 式(UTC)
毎朝9時0 0 * * *
毎日0時0 15 * * *(前日の15時)
月曜の朝8時0 23 * * 0(日曜の23時)

日付や曜日をまたぐときは、曜日の欄もずらす必要があります。最短の間隔は1分ごとです。手元では wrangler dev --test-scheduled で起動すると、決まった URL を叩いて scheduled() を試せます。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 10 / 14
  • 書き方:cron 式は UTC。日本時間から9時間引く
  • 0 0 * * *
  • Worker の scheduled()
  • 毎朝9時(日本)の集計
  • 時計は UTC(世界標準時)
  • */15 * * * *
  • 15分ごと
  • 0 15 * * *
  • 毎日 0時(日本時間)= 15時(UTC)
  • 並び:分 時 日 月 曜日。最短は1分ごと。タイムゾーンは指定できない
  • 「毎朝9時(日本)」は 0 0 * * *。ここを間違える人がとても多い。

料金と制限:Workers と同じ数え方+記録の保存

Workflows の料金は、基本的に Workers と同じ数え方(リクエストと CPU 時間)に、状態の記録を保存している量が加わります。さらに、2026年に料金の見直しがあり、有料プランでは実行した段の数でも数えられるようになりました。無料プランには込みの量があります。数え方が変わったばかりなので、使う前に最新の料金ページを必ず確かめてください。

項目中身
数え方CPU 時間・段の数・記録の保存量など
眠っている間コードは動いていない。同時に動く数にも数えない
上限1インスタンスの段の数、段が返す値の大きさ、同時に動く数など

Cron Triggers 自体には追加料金はかかりません。呼ばれた Worker の実行が、ふつうの Workers の料金で数えられます。ただし、1つの Worker に設定できる本数や、アカウント全体の本数には上限があり、無料プランでは少なめです。無料プランでは、1回の実行で使える CPU 時間もリクエストのときと同じく短いので、重い集計は Workflow に任せましょう。

※2026年9月時点の情報です。料金・上限は変わるので、公式の Pricing と Limits のページで確かめてください。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 11 / 14
  • 料金と制限:Workers と同じ数え方+記録の保存
  • Workflows
  • Workers の料金(CPU 時間など)で数える
  • 記録の保存
  • 段の結果を保存した量も数える
  • 2026年の見直し
  • 有料プランは段の数でも数えるように
  • Cron Triggers
  • 追加料金なし。本数に上限あり
  • 無料プラン
  • どちらも使える。上限は小さめ
  • 眠っている・イベント待ちのインスタンスは、同時に動く数には数えられない
  • 2026年9月時点。料金・上限は変わるので公式の Pricing / Limits で確かめる
  • どちらも無料プランで試せる。上限は公式の Limits で確かめる。

よくある落とし穴

Workflows と Cron Triggers でつまずきやすい点です。

落とし穴何が起きるかどうするか
段の外の副作用再開のたびに、段の外のメール送信や書き込みが何度も走る外に影響する処理は必ず step.do の中へ
段の名前が変わる日時や乱数を名前に入れると、再開時に記録と結び付かず、もう一度実行される名前は毎回同じ文字列にする
段の中の二重実行段が途中まで進んでから失敗すると、再挑戦で前半がもう一度走る段の中の処理も冪等に(24号と同じ考え方)
大きな戻り値記録の上限に当たるR2 や D1 に置いてキーを返す
cron を日本時間で書く9時間ずれて動くUTC に直して書く。コメントに日本時間を添える
段の外の Date.now()再開のたびに値が変わる時刻が必要なら段の中で取って返す

段の中の処理は「少なくとも1回」実行されます。支払いのように二重にしたくない処理は、決済サービスの冪等キー(同じキーの依頼は1回しか処理しない仕組み)を使いましょう。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 12 / 14
  • よくある落とし穴
  • 段の外でメール送信・書き込み → 再開のたびに何度も走る
  • 段の名前に日時や乱数を入れる → 再開時に記録と結び付かない
  • 段の中で1回だけのはずの処理 → 再挑戦で2回。冪等に作る
  • cron を日本時間のつもりで書く → 9時間ずれて動く
  • 大きな結果は R2 に置き、段はキーだけを返す
  • 段の外に副作用を書かない。段の名前を変えない。cron は UTC。

Queues・Workflows・Cron の使い分け

「後でやる仕事」を扱う機能が3つそろいました。役割は次のように分かれます。

機能得意なこと例
Queues(24号)1回で終わる仕事を、たくさん、確実にさばくメール1通、画像1枚の縮小
Workflows何段にも分かれ、途中で待ったり、前の結果を使ったりする仕事注文 → 支払い → 3日後のメール
Cron Triggers決まった時刻に始めること毎晩の集計、1時間ごとの掃除

3つは組み合わせて使うことが多いです。たとえば、Cron Triggers が毎晩0時に Workflow を起動し、Workflow の1段目で集計対象を洗い出し、2段目でたくさんの小さな仕事を Queues に入れる、という形です。

ほかの号とのつながりも見ておきます。段の結果の置き場には D1(23号)や R2(22号)、1つの対象の状態を順番に更新したいなら Durable Objects(21号)、コマンドでのデプロイや状態の確認は Wrangler(34号)、失敗の見張りには分析とログ(32号)を使います。Workflows は、内部では Durable Objects の上に作られています。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 13 / 14
  • Queues・Workflows・Cron の使い分け
  • Queues(24号)
  • Workflows
  • Cron Triggers
  • 1回で終わる仕事をたくさん
  • 何段もあり、途中で待つ
  • 決まった時刻に始める
  • 例:メール1通・画像1枚
  • 例:注文→支払い→3日後
  • 例:毎晩の集計
  • 組み合わせる:Cron が毎晩 Workflow を起動し、Workflow の段がキューに仕事を入れる
  • 1つの部屋の状態を順に更新したいなら Durable Objects(21号)
  • 1回で終わる仕事は Queues、何段もあるなら Workflows、定時は Cron。

まとめ:途中で落ちても、最後までやり遂げる

この号で見たことをまとめます。

項目内容
Workflows が要る理由何段もある処理を、途中で落ちても最後までやり遂げるため
仕組み段ごとに結果を記録し、再開時は記録のある段を飛ばす
段を書くstep.do("名前", 関数)。段の名前は毎回同じに
待つstep.sleep・step.sleepUntil・step.waitForEvent
やり直し段ごとに回数・間隔・時間切れを決める
注意段の外に副作用を書かない。段の中も冪等に
Cron Triggers決まった時刻に scheduled() を呼ぶ
cron 式UTC で書く。日本時間から9時間引く
使い分け1回の仕事は Queues、何段もあるなら Workflows、定時は Cron

次の26号では、すでにある Postgres や MySQL に Workers から速くつなぐ Hyperdrive を見ます。

  • Cloudflare のすべて 25 ― Workflows と Cron Triggers
  • 14 / 14
  • まとめ:途中で落ちても、最後までやり遂げる
  • 注文を確認
  • 支払いを確定
  • 3日待つ
  • レビュー依頼
  • 状態の記録(Cloudflare が保存する。落ちても消えない)
  • 注文 #42 OK
  • 支払い済み
  • 待ち終わり
  • 送信済み
  • step.do で段を分け、step.sleep / waitForEvent で待ち、再挑戦は段ごと
  • 毎日・毎時の仕事は Cron Triggers。書く時刻は UTC(日本時間 − 9時間)
  • Workflows は段ごとに記録して続きから。Cron は UTC の時計で起こす。

0 件のコメント

読み込み中…

権利侵害・不適切な内容の連絡: [email protected]