Skip to main content

Cloudflare Cron Trigger Not Running: Seven Causes and How to Rule Each Out

Cloudflare cron trigger not running? Check the handler, deploys that replace triggers, UTC, and the 1-means-Sunday weekday trap, then get alerted next time.

FLAREWARDEN
FlareWarden Team
9 min read

Your Worker answers HTTP requests fine, the schedule is right there in the dashboard, and nothing has happened at 3 a.m. for a week. A Cloudflare cron trigger not running rarely throws an error you’d see, because the failure is an event that never arrived. The causes below run roughly from quickest to rule out to hardest.

First: Did It Fire at All?

Cloudflare keeps an execution history for every Worker with triggers. In the dashboard, go to Workers & Pages, select the Worker, open Settings, and under Trigger Events select View events. That split matters more than anything else in this article:

  • No events at all. The trigger isn’t registered, or isn’t firing. Start with causes 1 to 4.
  • Events, all failing. The trigger works; your scheduled() handler is throwing or running out of resources. Go to cause 6.
  • Events at odd times, or on the wrong days. Your expression doesn’t mean what you think. Cause 5.

Two caveats from Cloudflare’s cron trigger documentation (checked September 2026). The page stores only the 100 most recent invocations, so an every-minute job shows you under two hours of history. And for a new Worker, or one you’ve just renamed, events can take up to 30 minutes to appear, so an empty page right after creating the Worker proves nothing yet.

1. There’s No scheduled() Handler

A cron trigger invokes your Worker’s scheduled() handler, not its fetch() handler. If the module exports only fetch, the trigger has nothing to call. This bites people who wrote their job as an HTTP route and then attached a schedule to the Worker, expecting the schedule to request that route.

The fix is a handler that does the work directly:

export default {
  async fetch(request, env, ctx) { /* your HTTP routes */ },

  async scheduled(controller, env, ctx) {
    await runNightlySync(env);
  },
};

You can test it locally without waiting for the clock. With wrangler dev running, request http://localhost:8787/cdn-cgi/local/scheduled and the handler fires once; add ?cron=0+3+*+*+* to simulate a specific schedule.

2. Your Last Deploy Replaced the Triggers

This one catches teams who mix the dashboard and Wrangler. Cloudflare’s docs say that if a Worker is managed with Wrangler, its cron triggers should be managed exclusively in the Wrangler configuration file, and that each Wrangler deploy replaces the previous triggers with the ones in the triggers array:

  • crons = [] removes every trigger on deploy.
  • Leaving triggers or crons out entirely leaves the deployed triggers alone.

So a trigger someone added by hand in the dashboard survives until the next deploy that includes a crons list, then silently disappears. The deploy succeeds, the site works, and the job simply stops. If your triggers vanished around the time of a release, check this first. Put them in wrangler.toml (or wrangler.jsonc) and treat the dashboard as read-only.

The reverse surprise exists too: commenting out the crons key does not disable a trigger, per the Wrangler configuration reference. And environments can override the list. triggers is inherited from the top level unless an [env.<name>.triggers] block replaces it, so check which list the environment you actually deployed to ends up with.

3. It Hasn’t Propagated Yet

Adding, changing, or deleting a cron trigger “may take several minutes (up to 15 minutes) to propagate to the Cloudflare global network,” per the same docs. A job scheduled for the next five minutes right after a deploy can legitimately be missed. Wait a quarter of an hour before debugging a brand-new trigger.

4. You’ve Hit the Account Limit

On the Workers Free plan, an account gets 5 cron triggers in total, across all Workers. Workers Paid raises that to 250. A team that’s been adding scheduled Workers one at a time on a free account runs into this without ever looking at a limits page. Count the triggers on every Worker in the account, not just the one that’s misbehaving.

5. UTC, and a Monday That Is Actually Sunday

Cron triggers run on UTC. That part is familiar. The weekday numbering is the trap.

In Cloudflare’s cron syntax, days of the week run 1 = Sunday to 7 = Saturday. Standard Unix cron, and most tools you’ve used, count 0 = Sunday and 6 = Saturday. So the expression most people write for “weekdays at 9”:

0 9 * * 1-5

runs Sunday through Thursday on Cloudflare. Friday’s job never happens and Sunday gets one nobody expected. Cloudflare’s own documentation recommends three-letter names to avoid exactly this ambiguity, and it’s good advice everywhere:

0 9 * * MON-FRI

The same trap works in reverse when you copy a schedule out of Cloudflare into another tool. FlareWarden’s cron monitors use the standard numbering (0 or 7 = Sunday), so a Cloudflare expression pasted in with numeric weekdays will expect runs on different days than the ones Cloudflare actually fires. Use names on both sides and the problem disappears.

6. It Runs but Dies Before Finishing

If events exist but fail, check the invocation status. Two limits end scheduled runs early:

LimitWorkers FreeWorkers Paid
CPU time per cron invocation10 ms30 seconds for schedules under an hour; 15 minutes for hourly or slower
Wall-clock duration per cron invocation15 minutes15 minutes

Ten milliseconds of CPU is enough for a job that mostly waits on the network, because time spent waiting on fetch(), KV reads, or database queries doesn’t count toward CPU time. It often isn’t enough for real computation: the same limits page says heavier workloads that parse large payloads or do server-side rendering typically use 10 to 20 ms per request, at or over the Free ceiling on their own. A job that passed testing against small data and started failing as the data grew is a likely candidate.

Promises are the other way work goes missing. The runtime waits for the promise your scheduled() handler returns, up to the 15-minute limit. Work you kick off without awaiting, or without passing to ctx.waitUntil(), isn’t something the runtime promises to wait for. Await it.

And note what the event log records as success. Per the scheduled handler docs, the first failing ctx.waitUntil() promise sets the invocation’s status; otherwise it’s reported as a success. A handler that catches its own exception and returns normally shows green in the history while doing nothing at all.

7. It Stopped, and You Changed Nothing

Cloudflare’s docs note that cron-scheduled Workers “run on underutilized machines” to make best use of capacity. Most of the time that’s invisible. But search the Cloudflare Community forum for cron triggers not firing and you’ll find a steady stream of threads describing triggers that stopped with no deploy on the owner’s side, or new accounts whose triggers were listed but never fired. None of those have a fix you can apply from your end beyond redeploying and contacting support. What you can control is how quickly you find out.

Getting Told When the Trigger Goes Quiet

Every cause above has one thing in common: nothing tells you. The Past Events page is a history you have to remember to open, and it can’t show an invocation that never happened in any way that alerts you.

The fix is to have the job report in, and have something notice when it doesn’t. That’s a cron monitor: FlareWarden gives you a unique ping URL, you tell it the schedule, and if the ping doesn’t arrive within the grace period the monitor goes Late and alerts. It’s push-based, so the Worker makes one outbound request and nothing needs access to Cloudflare. The call costs almost nothing against the Free plan’s CPU budget, since waiting on the network doesn’t count.

async scheduled(controller, env, ctx) {
  const ping = `https://app.flarewarden.com/ping/${env.FW_PING_TOKEN}`;
  await fetch(`${ping}/start`);
  try {
    await runNightlySync(env);
    await fetch(ping);
  } catch (err) {
    await fetch(`${ping}/fail`, { method: "POST", body: String(err) });
    throw err;
  }
},

The /start ping plus a max run duration catches the run that begins and never finishes, which covers a Worker killed at its limit. /fail accepts an error message in the body, so the alert says what broke instead of just “late”. And re-throwing keeps the failure visible in Cloudflare’s own event history too.

Enter the schedule in FlareWarden with weekday names, and set the grace period to a few minutes rather than seconds, since Cloudflare doesn’t promise to-the-second execution. If you’d rather not mirror the schedule at all, a plain interval works; the trade-offs between the modes are in our heartbeat vs cron vs uptime monitoring guide.

The Same Silence on Other Platforms

A Cloudflare cron trigger not running is one instance of a general rule: scheduled work fails by not happening, and platforms report on things that happened. Vercel cron jobs and Render cron jobs each have their own documented ways to go quiet, and the fix has the same shape on every one of them.


Key Takeaways

  • Check Past Events first. No events, failing events, and events at the wrong time are three different problems.
  • Triggers call scheduled(), not fetch(). A Worker without a scheduled handler has nothing for the trigger to run.
  • Wrangler deploys replace triggers. Dashboard-added triggers vanish on the next deploy that lists crons. Keep them in config.
  • 1 means Sunday. Cloudflare numbers weekdays 1 to 7 from Sunday, so 1-5 runs Sunday to Thursday. Use MON-FRI.
  • Free plan: 5 triggers per account, 10 ms of CPU per run. Network waits don’t count; parsing does.
  • Green in the event log isn’t proof of work. A handler that swallows its own errors reports success. Make the job check in.

Find out the night your trigger goes quiet, not a week later. Start monitoring free with FlareWarden: cron and heartbeat monitors on the free plan, 15 monitors, no credit card required.