Cloudflare Workflows vs a hosted scheduler: when Cron Triggers stop being enough
Cloudflare Workflows vs a hosted scheduler: when Cron Triggers stop being enough
If you already deploy to Cloudflare Workers, the honest starting position on Cloudflare Workflows vs a hosted scheduler is that you probably do not need the hosted one. A Cron Trigger costs nothing extra, runs on infrastructure you already trust, and takes four lines of config. Anybody selling you a scheduler should say that first.
The interesting question is where that stops working. It stops in a specific, documented place, and it is not the place most people expect. It is not price and it is not reliability. It is that Cron Triggers are declared at deploy time and capped per account, which means the moment your schedules belong to your users rather than to your app, the model runs out.
Every limit and price below was checked on 3 September 2026.
What Cloudflare Cron Triggers actually give you
A Cron Trigger is a cron expression in your Wrangler config that invokes your Worker's scheduled handler. Minimum granularity is one minute, everything runs in UTC, and it is free on the Workers Free plan.
The limits worth knowing before you build on it, from the Workers limits page:
| Workers Free | Workers Paid | |
|---|---|---|
| Cron Triggers per account | 5 | 250 |
| CPU time per cron invocation | 10 ms | 30s under 1h interval, 15 min at 1h or longer |
| Wall clock per invocation | 15 min | 15 min |
| Subrequests per invocation | 50 | 10,000, configurable higher |
Two of those catch people out. The first is that the Cron Trigger cap is per account, not per Worker. Five on Free is a number you can hit on a hobby account by accident. Two hundred and fifty on Paid sounds generous right up until you try to give every customer their own schedule, at which point it is a hard wall you cannot pay your way past.
The second is the propagation delay. Cloudflare's docs say adding, updating or deleting a Cron Trigger "may take several minutes (up to 15 minutes) to propagate". That is fine for a deploy. It is not fine as the response time of a "schedule this for me" API call.
Where Cloudflare Workflows fits
Workflows is the durable execution layer on top of Workers: your function is split into steps, each step's result is checkpointed, and a crash resumes from the last good step. For scheduling specifically, two properties matter.
step.sleep goes up to 365 days, and instances in a waiting state do not count toward your concurrency limit. So a workflow that sleeps for three weeks is not holding anything open. That is the property that makes Workflows a real scheduler and not just a retry wrapper, and it is why it belongs in any list of Inngest alternatives.
The rest of the limits:
| Workers Free | Workers Paid | |
|---|---|---|
| Steps per Workflow | 1,024 | 10,000 default, up to 25,000 |
| Concurrent instances | 100 | 50,000 |
| Executions | 100,000/day | Unlimited |
| Instance retention | 3 days | 30 days |
| Max retries per step | 10,000 | 10,000 |
Billing changed recently and is worth re-reading if you last looked before the summer. Workflows bills four dimensions: requests, CPU time, storage, and steps. Steps and storage only started billing on 10 August 2026. Free gives you 3,000 steps a day. Paid includes 500,000 steps a month, then $0.80 per additional 100,000. Idle time is explicitly free: a workflow sleeping or waiting on an API call does not accrue CPU time.
At the volumes most people reading this are running, steps are a rounding error. Five hundred thousand steps is a hundred thousand five step runs a month.
Cloudflare Workflows vs a hosted scheduler: the actual dividing line
Here is the split, and it has nothing to do with price.
Deploy-time schedules. Your app has a fixed set of recurring jobs. A nightly digest, an hourly sync, a weekly cleanup. The set changes when you ship. Cron Triggers are the correct answer and everything else is worse. Put the expression in Wrangler, done.
Runtime schedules. The schedules are created by something that is not you deploying. A user picks a reminder time. An agent decides mid-conversation that it should check a page tomorrow morning. A webhook asks for a retry in four hours. The count is unbounded and the creation is a request, not a deploy.
Cron Triggers cannot do the second one. There are exactly two ways to handle it on Cloudflare, and both are you building a scheduler:
- One cron plus a due table. A single Cron Trigger fires every minute, queries a table of pending jobs, and fans out the ones that are due. This works and plenty of production systems do it. You now own the polling loop, the claim-and-lock logic so two invocations do not double-fire, the retry ladder, the backoff, the dead letter path, and the alerting. That is a week of work and a permanent maintenance surface, for a feature that is not your product.
- A Workflow instance per job. Create an instance at runtime,
step.sleepuntil the fire time, then do the work. This is the better answer of the two and it is genuinely elegant, because waiting instances are free of concurrency cost and sleeps go out a full year. The catch is that Workflows runs your code on Cloudflare's compute, so this only exists if the thing being scheduled is a Worker you deploy. If the target is an endpoint somewhere else, you are deploying a Worker whose entire job is to sleep and then make one HTTP request.
A hosted HTTP scheduler is the third option: the schedule is a row in someone else's database, created by a POST, and the fire is an HTTP call to a URL you already have. You are not deploying anything. The trade is that you now depend on a service that is not Cloudflare, and you pay per call.
What each one costs
For the common case, one scheduled HTTP call per day:
| Runs per month | Cloudflare, if the scheduler is why you are on Workers Paid | Cloudflare, if you already pay for Workers | Hosted at $0.01 per run |
|---|---|---|---|
| 30 | $5.00 | ~$0.00 | $0.30 |
| 100 | $5.00 | ~$0.00 | $1.00 |
| 500 | $5.00 | ~$0.00 | $5.00 |
| 5,000 | $5.00 | ~$0.00 | $50.00 |
| 50,000 | $5.00 | ~$0.00 | $500.00 |
Read that table honestly. The break-even against the $5/month Workers Paid minimum is 500 runs a month, and above that Cloudflare wins and keeps winning, because its marginal cost per run is close to zero. If you are already paying the $5 for other reasons, the middle column is the real one and Cloudflare wins at every volume on price alone.
Per-call billing only wins in the left-hand column, at low volume, when the schedule is the only reason you would stand any of this up. That is a narrower claim than "cheaper", and it is the one that survives arithmetic. The general version of this math is in pay-per-use API pricing for AI agents.
For comparison, Upstash QStash sits between the two: $1 per 100,000 messages with 1,000 active schedules included on pay-as-you-go, but only 10 schedules on the free tier, and every delivery attempt bills as a message so retries cost extra.
When a hosted scheduler is the better call
Four cases, and they are all narrow:
- You are not on Cloudflare. Deploying a Worker so it can sleep and call your Vercel endpoint is a strange amount of machinery for one timer.
- The schedules are per user or per agent and unbounded. This is the 250-per-account wall. Either you build the due-table scheduler or you rent one.
- The scheduler is the only infrastructure in play. A side project with one daily job does not need a Workers Paid plan, a Workflow class, and a deploy pipeline.
- The thing creating the schedule is an agent. An LLM in Claude Desktop or Cursor cannot run
wrangler deploy. It can make a tool call. This is the case we go through in detail in cron for AI agents and MCP scheduler.
That last one is what we built Omniyond for, so read this section knowing who wrote it. You POST a URL and a cadence, it makes the call for $0.01 a run with a retry ladder of 30 seconds, then 5 minutes, then 30 minutes, and a dead letter queue behind that. The result is stored for you to fetch or POSTed to a callback.
curl -X POST https://omniyond.com/v1/schedule \
-H "Authorization: Bearer omni_live_xxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://api.acme.com/daily-digest","schedule":"1d","delivery":"callback","callbackUrl":"https://acme.com/hooks/digest"}'
The limits, plainly, because they matter more than the pitch. Recurrences are intervals from 5m upward, not cron expressions, so "every weekday at 09:00" is not expressible and neither are timezones. The horizon is 90 days, against Cloudflare's 365-day sleep. There are no steps, no fan-out and no flow control. If your job has branching in it, Workflows is a better tool and it is not close.
The short version
If your schedules are fixed and you already ship Workers, use a Cron Trigger and stop reading. If they are fixed but the job has real steps and branching, use Workflows, which is cheap, durable, and lets a sleeping instance sit for a year without holding concurrency.
If the schedules are created at runtime, you have chosen between building a due table behind one cron, running a Workflow instance per job, or renting a hosted scheduler. The first is more work than it looks, the second is excellent when the target is already a Worker, and the third is worth it mainly at low volume or when the thing doing the scheduling is not a deploy pipeline.
The number that decides it is 250. Below that, Cloudflare does this better and cheaper than anyone. Above it, Cron Triggers were never the design.
Ready to put your agent on a schedule?
Omniyond gives your AI agents a scheduler, utility tools, and pay-per-call x402 billing — no plan, no subscription.
