Skip to content

Cloudflare Limits

Cloudflare Workflows runs inside the Cloudflare Workers runtime. The following limits apply to all workflows deployed by AwaitStep.

Core Limits

LimitValueNotes
Max steps per workflow100Counts step.do() calls. sleep and sleepUntil do not count.
Max CPU time per step15 minutesActive JavaScript execution time. Awaiting I/O does not count.
Max wall time per step30 secondsReal elapsed time per step.do() call, including I/O wait.
Max total workflow duration1 yearClock time from first step to last.
Max concurrent instances1,000Per account. Includes all workflows on the account.
Max payload size1 MBApplies to workflow input and each step's return value.

WARNING

The 30-second wall time limit per step.do() call is the most commonly hit limit. If a single step makes a slow external API call, it can time out even if the CPU time limit is not reached. Design steps to be fast, or split long-running operations across multiple steps.

Step Limit (100 steps)

Every step.do() call counts against the 100-step limit. This includes steps generated by loops — each iteration of a loop node creates new steps at runtime.

Workaround for loop-heavy workflows:

If you need to process more than 100 items in a loop, batch them:

typescript
// Instead of one step per item (N steps):
for (const item of items) {
  await step.do(`process_${item.id}`, async () => processItem(item))
}

// Process items in batches (ceil(N/10) steps):
const batches = chunk(items, 10)
for (const [i, batch] of batches.entries()) {
  await step.do(`process_batch_${i}`, async () => {
    return Promise.all(batch.map(processItem))
  })
}

You can also use a sub-workflow to offload processing to a separate workflow instance, each with its own 100-step budget.

Wall Time Limit (30s per step)

Each step.do() call has a maximum wall time of 30 seconds. If your step makes a network request that takes longer, it will time out.

Workarounds:

  1. Split the operation. If you are polling an external API, use a sleep + loop pattern instead of a single blocking call.
  2. Use a webhook. Trigger the external operation and use wait_for_event to wait for a callback, rather than polling synchronously.
  3. Use a sub-workflow. Offload the long-running step to a child workflow with its own step budget.
typescript
// Pattern: poll with sleep
// Step 1: Kick off the job
const job_result = await step.do('start_job', async () => {
  return startExternalJob({ input: data })
})

// Step 2+: Poll until done (each poll is a separate step)
let status = 'pending'
let attempts = 0
while (status === 'pending' && attempts < 20) {
  await step.sleep(`wait_${attempts}`, '30 seconds')
  const poll_result = await step.do(`poll_${attempts}`, async () => {
    return checkJobStatus(job_result.jobId)
  })
  status = poll_result.status
  attempts++
}

CPU Time Limit (15 min per step)

The 15-minute CPU limit applies to active JavaScript execution. Waiting for network I/O (fetch, etc.) does not count against this limit.

In practice, the 30-second wall time limit is usually reached first for I/O-bound steps.

Payload Size (1 MB)

The 1 MB limit applies to:

  • The workflow input payload (passed at trigger time)
  • The return value of each step.do() call

Workarounds:

  • Store large data in KV, R2, or D1 and pass a reference (ID or key) between steps instead of the data itself.
  • Compress large payloads before storing them as step output.
typescript
// Instead of returning the full document (may exceed 1 MB):
return { document: largeDocumentString }

// Store in R2 and return a reference:
const key = `docs/${crypto.randomUUID()}`
await env.MY_BUCKET.put(key, largeDocumentString)
return { documentKey: key }

Concurrent Instances (1,000)

Cloudflare allows a maximum of 1,000 concurrently running workflow instances per account. This limit spans all workflows on the account.

Workaround: If you expect high concurrency, consider rate-limiting workflow creation at the API layer, or batching work into fewer, longer-running workflows.

Max Duration (1 Year)

A single workflow instance can run for up to 1 year from start to completion. This includes time spent paused in sleep or wait_for_event states.

This is more than sufficient for most use cases, including long-running approval workflows, scheduled reminders, and multi-day batch processes.

Summary Cheatsheet

What you want to doLimit to watch
Process a large list100 steps (each loop iteration is a step)
Make a slow API call30s wall time per step
Return large data from a step1 MB payload
Run many workflows in parallel1,000 concurrent instances
Build a year-long reminder workflow1 year max duration