Nullstead

Cloudflare

2026年版 Cloudflare Workflows実践設計: 個人開発の自動化を「壊れにくく」する4層アーキテクチャ

Cloudflare Workflowsを中心に、Durable Objects、Queues、Cron Triggersを組み合わせて、個人開発の自動化を壊れにくく設計する実践ガイドです。再試行、重複防止、コスト管理の勘所をコード付きで解説します。

約11分

失敗しにくい自動化は「速さ」より「復元力」から始める

個人開発で自動化を進めると、最初はだいたいこうなります。
「よし、夜間バッチも通知も全部自動にした。これで寝ている間に世界が良くなる!」
そして翌朝、静かに失敗ログを見てコーヒーを二杯飲むことになります。技術的には動いているのに、運用としては不安定。これは私だけではなく、多くの個人開発者が通る道です。

2026年現在、Cloudflare Workflowsがこの状況をかなり改善してくれる選択肢になってきました。Workersの上で、長時間のマルチステップ処理、再試行、状態保持、イベント待機をまとめて扱えるからです。要するに「途中で転んでも立ち上がれる自動化」を作りやすいわけです。
本記事では、Workflowsを中心に、Durable Objects、Queues、Cron Triggersをどう組み合わせると個人開発にちょうど良いかを、設計の勘所と実装例つきで整理します。

※ Workflows: Cloudflare Workers上で長時間・複数手順の処理を実行できる機能です。
※ Durable Objects: 単一スレッドで一貫性を保ちながら状態を扱える実行単位です。
※ at least once: メッセージが最低1回は届く保証。重複処理の設計が必要です。

2026年の前提整理: Workflowsは「実験」より「運用」寄りになった

Cloudflare公式ドキュメントとChangelogを見ると、WorkflowsはOpen BetaからGAを経て、実務投入向けの改善が進んでいます。特に効くのは次の3点です。

  1. step.waitForEvent が正式に使える
    外部イベントや人間承認を待って処理を再開できます。Webhook受信や審査フローに直結します。
  2. 長時間処理の前提が明確
    ステップ単位で再試行や待機を扱えるため、外部APIの瞬断で全体を落としにくいです。
  3. 制限値の現実解が増えた
    1インスタンスのステップ上限がデフォルト10,000、設定で25,000まで拡張可能になり、設計の余裕が増えました。

ここで重要なのは「無限に何でもできる」ではなく、「制限を理解すると壊れにくい設計がしやすい」に変わった点です。
個人開発だと機能追加より先に運用で消耗しがちなので、この差は地味に大きいです。

構成の基本形: Workerを入口にして、非同期を分離する

私が扱いやすいと感じるのは、以下の4層構成です。

  • 入口(HTTP Worker): 認証、入力検証、受け付け
  • 実行(Workflows): 手順管理、再試行、待機、進捗
  • 調停(Durable Objects): 重複排除、排他、状態集約
  • 緩衝(Queues): バースト吸収、外部APIへの送信平準化

この構成の狙いは、障害の種類ごとに責務を分けることです。
たとえば「同じWebhookが3回来た」はDurable Objectsで吸収し、「通知APIが一時的に遅い」はQueuesで吸収し、「承認待ちで数時間止めたい」はWorkflowsで吸収します。

言い換えると、全部を一つのfetchハンドラに押し込まない、です。
筋トレで言うと、毎日腕だけ鍛えるのをやめて、ちゃんと脚もやる感じです。システムは脚が大事です。

実装例1: 失敗しにくいワークフロー本体

まずはWorkflows本体の最小構成です。ここでは、注文イベントを受けて「課金確認 → 在庫確保 → 通知送信」を行う例にしています。

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

type Env = {
  ORDER_QUEUE: Queue;
  INVENTORY: DurableObjectNamespace;
};

export class OrderWorkflow extends WorkflowEntrypoint<Env> {
  async run(event: WorkflowEvent<{ orderId: string; userId: string }>, step: WorkflowStep) {
    const { orderId, userId } = event.payload;

    const payment = await step.do(
      "payment-check",
      { retries: { limit: 4, delay: "5 seconds", backoff: "exponential" } },
      async () => {
        const res = await fetch(`https://api.example.com/payments/${orderId}`);
        if (!res.ok) throw new Error("payment api failed");
        return await res.json<{ status: string }>();
      }
    );

    if (payment.status !== "paid") {
      await step.sleep("wait-retry-payment", "10 minutes");
      throw new Error("payment not confirmed yet");
    }

    await step.do("reserve-stock", async () => {
      const id = this.env.INVENTORY.idFromName(orderId);
      const stub = this.env.INVENTORY.get(id);
      await stub.fetch("https://do/reserve", {
        method: "POST",
        body: JSON.stringify({ orderId }),
      });
    });

    await step.do("enqueue-notify", async () => {
      await this.env.ORDER_QUEUE.send({ orderId, userId, type: "order-confirmed" });
    });
  }
}

ポイントは3つです。

  • step.doを小さく切る: 失敗時の再実行範囲が明確になります。
  • 再試行対象を選ぶ: 一時障害にだけリトライし、論理エラーは即失敗させます。
  • 副作用は外出し: 通知送信などはQueueに渡し、ワークフロー本体を軽く保ちます。

※ 再試行で同じ処理が複数回走る可能性があるため、下流は冪等性を前提に設計します。
※ 冪等性: 同じ入力を複数回処理しても結果が変わらない性質です。

実装例2: Durable Objectsで重複処理を止める

Queuesは少なくとも1回配送です。つまり、同じメッセージを複数回処理する可能性があります。ここでDurable Objectsを「重複ガード」に使うと、かなり平和になります。

import { DurableObject } from "cloudflare:workers";

export class NotificationGate extends DurableObject {
  async fetch(req: Request): Promise<Response> {
    const { messageId, payload } = await req.json<{ messageId: string; payload: unknown }>();

    const row = this.ctx.storage.sql
      .exec("SELECT processed_at FROM dedupe WHERE message_id = ?", messageId)
      .toArray();

    if (row.length > 0) {
      return new Response("duplicate", { status: 200 });
    }

    await this.ctx.storage.sql.exec(
      "INSERT INTO dedupe (message_id, processed_at) VALUES (?, ?)",
      messageId,
      Date.now()
    );

    // ここで実際の通知処理を実行
    // 失敗時は例外を投げることで上位リトライと連携

    return new Response("ok", { status: 200 });
  }
}

Durable Objectsは単一スレッドで処理されるため、同じキーに対する同時更新でロック地獄になりにくいです。
ただし万能ではありません。負荷が高い場合はシャーディング設計が必要です。公式ガイドでも「何を1インスタンスに対応させるか」が最重要とされています。

実装例3: Cronは「起動の合図」、本体はWorkflowsへ

定期処理をCron Triggersだけで完結させる設計は、最初は簡単ですが拡張時に苦しくなります。理由はシンプルで、Cron実行には壁時計時間の制約があるためです。
そこでおすすめなのが、Cronは「開始イベントを投げるだけ」にする形です。

export default {
  async scheduled(controller: ScheduledController, env: Env): Promise<void> {
    const iso = new Date(controller.scheduledTime).toISOString();
    await env.DAILY_WORKFLOW.create({
      id: `daily-${iso}`,
      params: { scheduledAt: iso },
    });
  },
};

この分離をしておくと、次の利点があります。

  • バッチが長引いても、Cronハンドラは短時間で終了できる
  • 失敗時の再開ポイントをWorkflows側で持てる
  • テストが楽になる(Cronは起動確認、処理本体は別テスト)

なお、Cron設定変更は反映まで最大15分程度かかることがあるため、デバッグ時はwrangler devのscheduledエンドポイントを使うと事故が減ります。

コストと制限で先に詰まらないための設計メモ

2026年時点で、Workflowsの課金はWorkers標準価格に準じ、主に「リクエスト」「CPU時間」「保持ストレージ」で構成されます。ここで誤解しやすい点を先に書いておきます。

  • 待機中はCPU課金されない
    step.sleepや外部応答待ちはCPU消費に含まれません。
  • ステップは請求上のリクエスト増分ではない
    新規インスタンス起動が主な単位です。
  • 状態保持は積み上がる
    完了済みインスタンスの保持期間が長いと、地味にストレージを使います。

実践上は、次の3つを最初から決めるのが効きます。

  1. 完了インスタンスの保持方針(短めにするか)
  2. 1インスタンスあたりの最大ステップ想定
  3. 外部API呼び出し回数の上限(サブリクエスト制御)

個人開発で怖いのは、ピーク時より「普段のじわじわ課金」です。
静かに太るコストは、静かに太る体重と同じで、ある日突然見ないふりができなくなります。

まとめ: 自動化は「完璧な一発」より「失敗前提の連携」で勝つ

Workflows、Durable Objects、Queues、Cron Triggersを組み合わせると、個人開発でもかなり実運用寄りの自動化が作れます。ポイントは、機能を増やすことより「失敗の受け皿」を増やすことです。

  • Workflowsで手順と再開を管理する
  • Durable Objectsで重複と整合性を守る
  • Queuesでバーストと外部API遅延を吸収する
  • Cronは開始トリガーに限定する

この形にしておくと、深夜の通知が鳴る回数は確実に減ります。
ゼロにはなりませんが、ゼロを目指して眠れなくなるより、2回を1回に減らすほうが現実的です。個人開発は長距離戦なので、まずは壊れ方をデザインしていきましょう。


参考リンク

Tags #Cloudflare #Workflows #自動化 #個人開発 #Durable Objects