Cloudflare Workflows Free Plan: Retries, Steps and Fatal Errors

A pipeline that calls an AI model, saves an image and publishes a page can fail in the middle for dull reasons: a slow response, a malformed answer, a quota that ran out. Cloudflare Workflows exists for this. You write the pipeline as a sequence of named steps, the platform stores each step's result and retries failed steps for you. It works on the free plan, and it is what runs our production content cycle every three hours. This guide covers the structure that worked, the one wrapper that matters most, and the gotchas.
The shape of a Workflow
wrangler.toml declares the Workflow and binds it to your Worker:
[[workflows]]
name = "demo-workflow"
binding = "DEMO"
class_name = "Demo"
The class extends WorkflowEntrypoint, and run() is a sequence of step.do(name, config, fn) calls. Each step returns a value that Cloudflare persists, so a retry resumes after the last successful step instead of starting over. You start an instance from any Worker or Durable Object:
const inst = await env.DEMO.create({ id: "demo-2026-10-02-ab12", params: { topic: "x402" } });
Instance ids must be unique. We build ours from a timestamp plus four random characters.
Why small steps matter on the free plan
Every step.do runs in its own invocation, with a fresh CPU budget and a fresh count of subrequests. That is the trick that lets a long job fit into the free plan's per-request limits. Our article generator is about ten steps: one for the plan, one for the introduction, one per section, one for the image and one to save. If you put the whole job in one step, it must fit in a single invocation's limits, and a failure near the end repeats everything, including the expensive AI calls.
Two rules follow:
- Keep step results small. Store big things (images, audio) in KV or R2 inside the step and return only the key. Our image step writes the bytes to KV and returns
{ file, w, h }. - Make every step safe to repeat. A step may run twice. Our save step replaces the index entry for a slug if it already exists, so a retry never publishes a duplicate.
The wrapper that saves your retries
By default, any thrown error is retried according to the step's config. That is right for a timeout or a bad response, and wrong for "the daily AI allowance is used up", which will fail the same way three more times and make the whole instance take minutes to die. The platform has an escape hatch: throw NonRetryableError, and the instance stops at once. We wrap step.do so that our own errors can ask for that without importing Cloudflare classes everywhere:
import { WorkflowEntrypoint } from "cloudflare:workers";
import { NonRetryableError } from "cloudflare:workflows";
// An error you flag yourself: retrying will not help.
class FatalError extends Error {
constructor(message) { super(message); this.fatal = true; }
}
const RETRY = { retries: { limit: 2, delay: "20 seconds", backoff: "exponential" }, timeout: "8 minutes" };
export class Demo extends WorkflowEntrypoint {
async run(event, step) {
const safe = (name, fn) =>
step.do(name, RETRY, async (ctx) => {
try {
return await fn(ctx);
} catch (e) {
if (e && e.fatal) throw new NonRetryableError(String(e.message));
throw e;
}
});
const plan = await safe("plan", async () => ({ sections: 3 }));
const parts = [];
for (let i = 0; i < plan.sections; i++) {
parts.push(await safe(`section-${i}`, async (ctx) => `section ${i} (attempt ${ctx.attempt})`));
}
return await safe("save", async () => ({ saved: parts.length }));
}
}
We ran this pattern in wrangler dev with three scenarios, using a one-second retry delay to keep the test short:
- Normal run: complete in 0.6 seconds.
- A transient failure in section 1: attempt 1 threw, attempt 2 succeeded, and the output showed
section 1 (attempt 2). The instance completed in 1.1 seconds, one retry delay later. - A
FatalErrorin section 2: statuserroredafter 0.6 seconds, with the message "The execution of the Workflow instance was terminated, as a step threw an NonRetryableError and it was not handled". No retries were spent.
What to mark as fatal
- A used-up quota. Workers AI answers with an error that mentions the daily free allocation (code 4006). Match it and stop: nothing will work until 00:00 UTC.
- A missing binding or configuration. Retrying a missing AI binding is pointless.
- A quality gate that failed. If a generated article is too short or repeats itself, another attempt means another expensive generation. We reject it and let the next scheduled run try again with a fresh topic. For a malformed JSON answer, in contrast, a retry is often exactly right.
Gotchas
- Deploy lag. An instance created in the first minute or two after a deploy can still run the old code. Wait before you test, and do not conclude that your fix failed.
- Wall-clock timeouts are per step. Our default is 8 minutes for steps that call a model and 2 minutes for quick ones. A model that hangs should fail its step, not the instance.
- Look at instances, not just logs. The instance status shows which step failed and with what message. In
wrangler devyou can poll it withawait env.DEMO.get(id)followed by.status(); in production the Workflows REST API lists recent instances for your account. - Free-plan limits move. Check the limits page for the maximum steps per instance and concurrent instances before you design around a number. When we checked in September 2026, 1,024 steps and 100 concurrent instances were allowed on the free plan.
Where this fits
A Workflow does the work, and something has to start it on schedule. We use a Durable Object alarm for that, described in Cloudflare Cron Triggers Not Firing? Use a Durable Object Alarm. The AI calls inside the steps have a daily budget, which is worked out in Workers AI Free Tier: What Each API Call Really Costs in Neurons.
FAQ
How do I stop a Cloudflare Workflow from retrying a step that can never succeed?
Throw a NonRetryableError (imported from cloudflare:workflows) inside the step. The instance stops immediately with status errored instead of spending its retries. We tested it: a retryable error with limit 2 and exponential delay takes seconds, a NonRetryableError ended the instance in under a second.
What limits apply to Workflows on the free plan?
As of our research in September 2026 the free plan allows up to 1,024 steps per instance and 100 concurrent instances, and each step runs with the normal Workers CPU limit for the plan. Check the current limits page before you depend on exact numbers.
Should every step be idempotent?
Yes. A step can run more than once (retries), so make it safe to repeat: for example, when saving a post, replace the existing index entry for the same slug instead of adding a second one.
Why did my new Workflow code not run right after a deploy?
In our experience an instance created within the first minute or two after a deploy can still run the previous version of the code. Wait a couple of minutes before testing a fresh deploy.
Found this useful? Tip the studio in crypto
Every EVM chain works. USDC on Base is recommended: fees are a fraction of a cent. No account needed — it goes straight to the creator's wallet.
0x13dd72Fa0E7504790585D92bD98c720f6fD2aBa6More from DevNotes

How to Add x402 Payments to a Cloudflare Worker with Hono
A tested, minimal example of charging per request in USDC on Base with x402, Hono and Cloudflare Workers…

How an AI Agent Pays an x402 API: a 20-Line Client in JS
A working x402 client in JavaScript: sign the USDC payment, read the receipt, cap what your agent can spend…

Cloudflare Cron Triggers Not Firing? Use a Durable Object Alarm
A reliable clock for Cloudflare Workers: a Durable Object alarm that re-arms itself, with a minimum gap and a…

How to List Your x402 API on 402 Index, x402scan and Bazaar
The exact steps to get a pay-per-call x402 API discovered by AI agents: OpenAPI metadata, 402 Index, x402scan…