Cloudflare
2026年版 Cloudflare Workflows実践設計: 個人開発の自動化を「壊れにくく」する4層アーキテクチャ
Cloudflare Workflowsを中心に、Durable Objects、Queues、Cron Triggersを組み合わせて、個人開発の自動化を壊れにくく設計する実践ガイドです。再試行、重複防止、コスト管理の勘所をコード付きで解説します。
失敗しにくい自動化は「速さ」より「復元力」から始める
個人開発で自動化を進めると、最初はだいたいこうなります。
「よし、夜間バッチも通知も全部自動にした。これで寝ている間に世界が良くなる!」
そして翌朝、静かに失敗ログを見てコーヒーを二杯飲むことになります。技術的には動いているのに、運用としては不安定。これは私だけではなく、多くの個人開発者が通る道です。
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点です。
- step.waitForEvent が正式に使える
外部イベントや人間承認を待って処理を再開できます。Webhook受信や審査フローに直結します。 - 長時間処理の前提が明確
ステップ単位で再試行や待機を扱えるため、外部APIの瞬断で全体を落としにくいです。 - 制限値の現実解が増えた
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インスタンスあたりの最大ステップ想定
- 外部API呼び出し回数の上限(サブリクエスト制御)
個人開発で怖いのは、ピーク時より「普段のじわじわ課金」です。
静かに太るコストは、静かに太る体重と同じで、ある日突然見ないふりができなくなります。
まとめ: 自動化は「完璧な一発」より「失敗前提の連携」で勝つ
Workflows、Durable Objects、Queues、Cron Triggersを組み合わせると、個人開発でもかなり実運用寄りの自動化が作れます。ポイントは、機能を増やすことより「失敗の受け皿」を増やすことです。
- Workflowsで手順と再開を管理する
- Durable Objectsで重複と整合性を守る
- Queuesでバーストと外部API遅延を吸収する
- Cronは開始トリガーに限定する
この形にしておくと、深夜の通知が鳴る回数は確実に減ります。
ゼロにはなりませんが、ゼロを目指して眠れなくなるより、2回を1回に減らすほうが現実的です。個人開発は長距離戦なので、まずは壊れ方をデザインしていきましょう。
参考リンク
- Cloudflare Workflows Overview
https://developers.cloudflare.com/workflows/ - Cloudflare Workflows Limits
https://developers.cloudflare.com/workflows/reference/limits/ - Cloudflare Workflows Pricing
https://developers.cloudflare.com/workflows/reference/pricing/ - Cloudflare Changelog: Workflows GA
https://developers.cloudflare.com/changelog/post/2025-04-07-workflows-ga/ - Cloudflare Queues: How Queues Works
https://developers.cloudflare.com/queues/reference/how-queues-works/ - Cloudflare Durable Objects: Rules of Durable Objects
https://developers.cloudflare.com/durable-objects/best-practices/rules-of-durable-objects/ - Cloudflare Workers Cron Triggers
https://developers.cloudflare.com/workers/configuration/cron-triggers/