Skip to content
Fig. 00 — Note 06 · Durable workflowsJanuary 2026 · 10 min

Durable workflows

Durable workflows · Cloudflare Workflows and AWS Durable Functions

Same problem, two products, one naming trap. A ticket reservation with a fifteen minute hold, built on both platforms, and the limits and gotchas that decide which one you pick.

Notes / durable-workflowsfawzyatwa.com
Note sheetFile 06 / 07 · Notes
Date

January 2026

Reading time

10 min

Slugwriting / durable-workflows
Share this note/writing/durable-workflows
LinkedInX

captured against cloudflare workflows GA · aws lambda durable functions · january 2026

When AWS announced Lambda Durable Functions, my first thought was: finally, AWS's answer to Cloudflare's Durable Objects, the main ingredient behind the Agents framework.

That was not the case. The real match is Cloudflare Workflows vs AWS Durable Functions. Durable Objects is a different product entirely (stateful actors, per instance storage), and the naming doesn't make life easier for anyone drawing architectures across AWS, Azure, and Cloudflare.

To get a real feel for the two, I built the same workflow on both platforms and turned it into a hands on breakdown. The demo: a ticket reservation with a fifteen minute hold. A customer reserves a seat, has fifteen minutes to complete payment, and if they don't the seat auto releases. Same pattern that runs behind every event ticketing site.

This is the writeup. Full code lives on GitHub: fawzy-tat/cloudflare-vs-aws-durable-workflows.

Fig. 01 · two products, one problem shape

Cloudflare Workflows
class based · workers runtime · ts first
AWS Durable Functions
handler wrapper · lambda runtime · ts · py · java

What both solve

Before the syntax fight, the shared ground. Both products answer the same question: how do you write a multi step process that survives restarts, retries individual steps without redoing everything, waits for hours or days without burning compute, and keeps its state on disk somewhere the runtime can pick it back up?

The three primitives in Fig. 02 are all you need. Run some work and checkpoint the result. Sleep for a while. Wait for an external event. Both platforms ship the first two. Only Cloudflare ships a first class version of the third.

Fig. 02anatomy of a durable workflow · three step primitives, one checkpoint log
triggerhttp · queue · cronstep.dorun work · checkpointedstep.sleepminutes · days · a yearstep.waitForEventwebhook · approvalresultdurable statecheckpoint log · replay ready
Margin Note · primer · what does durable mean here?note
Your code can be interrupted at any moment (server restart, deploy, region evacuation) and pick up exactly where it left off. The runtime writes each step's result to a checkpoint log before moving on. When execution resumes, it replays the log and substitutes stored results for completed steps instead of running them again.
Margin Note · primer · what is checkpoint and replay?note
A workflow is deterministic code that talks to the outside world only through steps. Every step return value gets written to a log. On restart the runtime runs your code from the beginning, but each step call first checks the log. If a matching result is there, the step returns the stored value without running its body. If not, it runs and records the result. This is why step bodies must be idempotent, or must own the "did I already do this" check themselves.
Margin Note · billing · why is sleep free?billed
Both platforms suspend the workflow to disk during a sleep and release the compute. When the sleep expires, the runtime wakes the workflow and replays the checkpoint log to the point right after the sleep. You pay for the disk state, not the wall clock. A workflow can sleep for a year on either platform for the price of storing a few kilobytes.

The syntax · same patterns, different words

Read Listing 01 by tab, not by row. Each tab is a complete picture of that platform's three primitives.

worker/primitives.ts
class based
// Cloudflare Workflows — three primitives

// 1. step.do — run work and checkpoint the result
const charge = await step.do("charge card", async () => {
  return await stripe.charges.create({ amount, customerId });
});

// 2. step.sleep — free suspend for a duration
await step.sleep("cool off", "15 minutes");

// 3. step.waitForEvent — pause until an external event, with a timeout
const approval = await step.waitForEvent<Approval>("await review", {
  type: "review.approved",
  timeout: "24 hours",
});

The shape is identical across the two. A step that returns a checkpointed value, a wait that suspends, and a way to react to external events. Where they diverge is on the third one. Cloudflare's step.waitForEvent is a real primitive with a first class timeout. AWS does not ship an equivalent in the Node SDK today. The common pattern is to wait for the timeout window and then read state from your own store to decide what happened, or to route the callback through Step Functions and let it invoke the function when the token comes back.

That gap sounds small on paper. In practice it changes how you model any workflow that reacts to a webhook.

Margin Note · primer · wait vs waitForEvent · why it mattersnote
A wait for time is a scheduled resume. A wait for event is a listener with a deadline. If the event fires early, the workflow wakes early. If it never fires, the timeout still resumes you. Modelling both with a plain wait means the workflow always sleeps the full deadline and reads state on the way out, even when the answer arrived after two seconds.

A real workflow · the fifteen minute hold

The demo. A customer POSTs to /reserve with a seat id. The workflow marks the seat as HOLD, waits for the payment webhook, and either confirms the seat or lets it auto expire. Fig. 03 draws the flow once because both platforms implement the same shape.

Fig. 03ticket reservation · reserve, wait fifteen minutes, confirm or expire
POST /reservecustomer · seat idstep.domark seat · HOLDstep.waitForEventpayment.completed · 15m timeoutstep.docommit sale · charge cardstep.doauto expire · notify buyereventtimeout

The interesting branch is the timeout one, drawn with the accent stroke. Without durable execution, "auto expire in fifteen minutes" is a cron sweep, a scheduled task queue, or a Redis key with a TTL and a listener. With durable execution it is the else branch of a single line of code.

Listing 02 is the same shape written out in both dialects.

worker/reservation.ts
waitForEvent + timeout
// worker/reservation.ts
export class TicketReservation extends WorkflowEntrypoint<Env, Params> {
  async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
    const { customerId, seatId } = event.payload;

    await step.do("hold seat", async () => {
      await this.env.SEATS.put(seatId, JSON.stringify({
        status: "HOLD", customerId, at: Date.now(),
      }));
    });

    try {
      await step.waitForEvent<PaymentEvent>("await payment", {
        type: "payment.completed",
        timeout: "15 minutes",
      });
      await step.do("confirm", async () => {
        await this.env.SEATS.put(seatId, JSON.stringify({
          status: "CONFIRMED", customerId,
        }));
      });
    } catch {
      await step.do("release", async () => {
        await this.env.SEATS.delete(seatId);
        await notifyBuyer(customerId, "seat released");
      });
    }
  }
}

The Cloudflare version reads the way you'd sketch it on a whiteboard. Wait for the event, or fall through to the catch block on timeout. The AWS version has to sleep the full fifteen minutes and then read the state your payment webhook wrote. That difference costs nothing at fifteen minutes. At fifteen seconds it costs a lot: the AWS workflow always waits the full deadline, even when the customer paid immediately.

Margin Note · gotcha · side effects belong inside step.dowatch
The reserve, confirm, and release blocks all wrap their DB writes inside a step. That is not a stylistic choice. Anything outside a step runs again on every replay. A stray await ddb.put(...) between two steps will fire twice, three times, once per replay, and quietly corrupt the seat state.

Limits at a glance

Five rows that decide most of the picking. Numbers below are the ones I measured or verified against the docs at time of writing; both platforms move fast, so double check the pages in Cloudflare's limits reference and the Lambda Durable Functions guide.

Fig. 04 · limits at a glance · what the docs say today5 rows
dimensionCloudflare WorkflowsAWS Durable Functions
memory ceiling128 MB per instanceup to 10 GB per function
cold start≈ 1 to 5 ms (isolate)≈ 50 to 200 ms (container)
concurrent instancesup to 50,000 per account (paid)≈ 1,000 per region (reserved)
max sleep or wait365 days365 days
sleep costfree while suspendedfree while suspended

The memory row is the most decisive one. 128 MB is enough for orchestration, JSON reshaping, HTTP calls, and small batches. It is not enough to hold a big pandas frame or run a PDF through OCR in the same process. On AWS you can size the function up to 10 GB and keep the heavy lifting inside the workflow. On Cloudflare you push the heavy work down to a different service (a container, an R2 pipeline, an external worker) and orchestrate from the workflow.

Cold start matters when the workflow chain is short. A five step workflow that runs end to end in two seconds pays the cold start once. A workflow that fans out to a hundred small steps pays it a hundred times if each step wakes a fresh container.

Gotchas on the way

The things that don't show up until you deploy. Some are universal to durable execution as a category; some are platform specific.

Fig. 05 · gotchas · what bites and where7 rows
gotchawhere it bites
determinism requiredBoth. Step return values must be JSON serializable and the code outside steps must be deterministic (no Date.now(), no Math.random(), no environment reads that can change between replays). Sneaks up on you the first time you log a timestamp outside a step.
side effects inside step.doBoth. A DB write or HTTP call floating between two steps runs on every replay. Wrap it in a step or move it inside the previous one. Silent corruption when you miss it.
no first class waitForEventAWS Durable Functions. There is no context.waitForEvent in the Node SDK today. Model event waits as context.wait plus a state read, or bounce through Step Functions callback tokens. Cloudflare ships this as one call.
memory ceilingCloudflare. 128 MB per instance is the isolate limit. Anything heavier (image processing, large DataFrames) moves out to a separate service. AWS lets you size the function up to 10 GB in the same process.
cold start deltaAWS. First hit is 50 to 200 ms per fresh container. Reserved concurrency and provisioned concurrency help. Cloudflare's isolate warm up is 1 to 5 ms. Matters most when the step chain is short and latency is visible to a user.
concurrency ceilingAWS. Around 1,000 concurrent executions per region by default (raise via a support ticket). Cloudflare's paid plan tops out at 50,000 concurrent instances per account. Matters when a single event fan out lights up thousands of workflows at once.
the naming trap in searchBoth. Searching "AWS Durable" turns up Durable Objects tutorials that don't apply. Searching "Cloudflare workflow" turns up general purpose orchestrators (Airflow, Temporal, GitHub Actions). Add the vendor name to every query. Learned this the hard way.

Reach for X when…

Fig. 06 · the picker

Cloudflare Workflows
fast iteration · edge latency · ship in minutes
AWS Durable Functions
aws native · memory headroom · service integrations

Reach for Cloudflare Workflows when you want the shortest path from idea to a running orchestrator. You are already on Workers, or want to be. You value edge latency, you want step.waitForEvent as a first class primitive, and you can live inside 128 MB per instance by pushing the heavy lifting to R2, D1, or a separate container. The paid plan gives you 50,000 concurrent instances and a proper GA product.

Reach for AWS Durable Functions when you are deep in AWS and want to stay there. You need more than 128 MB of memory in the same process (image processing, ML inference, heavy transforms). You want to compose with EventBridge, Step Functions, SQS, or a hundred other AWS services without leaving IAM. You can absorb the extra 50 to 200 ms of cold start and the 1,000 per region concurrency ceiling.

Closing

Both products are the same category. Both give you steps, waits, and a checkpoint log. Pick the platform, not the primitive. Once you land on a platform, the workflow code looks about the same as anywhere else, and porting between them the day the tradeoff changes is a mostly mechanical exercise.

One last thing. If you take one thing from this piece, let it be that Durable Objects and Durable Functions are not the same thing, and neither is the closest match for the other. The naming is what it is. When you draw architectures across clouds, spell the whole product out. Your future self, and whoever reads the diagram after you, will thank you.