Using AI

Cloudflare Workers Cron Triggers Limit: 5 on Free, 250 on Paid (Official Docs) and 4 Ways Around It

Cloudflare Workers Cron Triggers Limit: 5 on Free, 250 on Paid (Official Docs) and 4 Ways Around It

Good morning, this is まさきん.

The official Cloudflare Workers docs cap Cron Triggers at 5 on the free plan. I run our home automation on the free tier, and the other day I hit that limit. Here are the ways I worked around it, checked against the official documentation.

※This information is current as of publication. Please check the official websites for the latest conditions.

POINT
  • On Workers Free, you get up to 5 Cron Triggers for the whole account
  • Times for similar jobs can be combined into one expression and routed inside the handler
  • If you move only the trigger to something like a home PC, the processing can stay in the Worker
  • If you call it from outside, you need secret-token authentication and a proper timeout
  • If that still is not enough, moving to the paid plan with 250 triggers is the straightforward option

How I hit the limit

I added 3 Cron Triggers to a Worker that sends health data notifications and deployed it. Only the schedule setup failed. The error code was 10072, and the cause was hitting the Workers Free account limit.

The code itself had deployed fine. That is what makes it a bit tricky: at first glance it looks like a success. If I had not checked the logs, I might have ended up with a mystery of “why isn’t this running?”

The free tier limit is 5

According to the official Limits page, the number of Cron Triggers is set per account. The key point is that it is not per Worker.

ItemWorkers FreeWorkers Paid
Number of Cron Triggers (whole account)5250
CPU time per Cron invocation10ms30 seconds (intervals under 1 hour) / 15 minutes (intervals of 1 hour or more)
CPU time per HTTP invocation10msUp to 5 minutes (default 30 seconds)
RequestsUp to 100,000 per day (resets at 0:00 UTC)10 million per month included

Home automation tends to be split into small Workers, one per purpose. If each Worker has 2 or 3 schedules, 5 fills up fast. In my case, it was full before I knew it.

A quick review of Cron Triggers basics

Before thinking about workarounds, let me go over the spec. Once you have these down, a range of options opens up.

The UTC part is the one to watch. Japan time is 9 hours ahead of UTC. If you want something to run at 6:30 in the morning, you write it as 21:30 on the previous day in UTC.

controller.cron contains the expression string exactly as you set it. The official docs say it has to match character for character, spaces included. Code written with the older event.cron follows the same idea.

Four main ways out

Here is a table of the options I considered, up front.

WorkaroundSlots savedProsCons
(1) Combine multiple times into one expressionLargeNo extra setup neededYour code has to route by time
(2) Move only the trigger outsideLargeYou can freely increase the frequencyStops working if the external trigger stops
(3) Consolidate into a dispatcher WorkerLargeStays entirely within CloudflareA bug in one place affects everything
(4) Move to the paid planSolves the root problemThe limit expands to 250Costs from $5 per month

Try (1) first, move on to (2) or (3) if that is not enough, and consider (4) last. If you are making do with the free tier, that order is probably the natural one.

Workaround 1: Combine times into one expression

This is the easiest one. Say you had separate expressions for morning and noon. Within the same Worker, you can join the hour field with a comma to make it one.

I combined my morning and noon expressions into this single one and redeployed.

[triggers]
crons = ["30 3,21 * * *"]

21:30 UTC is 6:30 in the morning Japan time. 3:30 UTC is 12:30 in the afternoon Japan time. That freed up 1 slot.

With a single expression, controller.cron cannot tell you which time fired. So you take the UTC “hour” from the scheduled time and route based on that.

export default {
  async scheduled(controller, env, ctx) {
    const hourUtc = new Date(controller.scheduledTime).getUTCHours();
    if (hourUtc === 21) {
      await sendMorning(env); // 6:30 Japan time
    } else if (hourUtc === 3) {
      await sendNoon(env); // 12:30 Japan time
    }
  },
};

scheduledTime is the time the invocation was scheduled for. I think checking this is more reliable than using the time it actually ran.

However, you can only combine times that share the same “minute.” If the minutes differ, like 6:30 and 12:15, you cannot express them in one expression. In that case, it is worth considering whether you can align the minutes.

If it is still full after combining

In my case, even after combining morning and noon, all 5 slots were still taken. There was no room to add anything new.

So I reviewed the remaining crons. One was for location sync, and two were for updating our home signage. All three assumed my Mac at home was running. So I chose to move the trigger outside.

Home automation ultimately runs over network connections. Having your connectivity in good shape makes it easier to run setups like this with peace of mind.

Try the fee simulator and get 100 points, no sign-up required Review the phone bill behind your home automation with Rakuten Mobile

Clicking this opens the Rakuten login page. Once you log in, you’ll see the campaign details.

Workaround 2: Move only the trigger outside

The idea is simple. Keep the processing in the Worker, and hand off only “when to run it” to something outside. The external trigger just calls the Worker’s URL over HTTP.

I removed the crons for location sync and signage updates. Instead, I switched to having launchd on my home Mac call the Worker’s URL. That brought my free slots to 3.

There was a nice side effect too. Location sync used to run once a day, and now I could increase it to every hour from 8:05 in the morning to 20:05 at night. Not having to worry about cron slots makes a big difference.

Build an endpoint on the Worker side

On the Worker side, prepare a path for triggering. The important thing is to let through only callers who know the password.

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname !== "/run/location-sync") {
      return new Response("not found", { status: 404 });
    }
    const auth = request.headers.get("Authorization");
    if (auth !== `Bearer ${env.RUN_TOKEN}`) {
      return new Response("unauthorized", { status: 401 });
    }
    await syncLocation(env);
    return new Response("ok");
  },
};

Do not write the token directly in your code; register it as a secret. Use npx wrangler secret put RUN_TOKEN to register it. In your code, you can read it with env.RUN_TOKEN.

Call it from the Mac with launchd

On the Mac side, a launchd job runs curl. The flow looks like this.

  1. Create a shell script that loads the token and calls the Worker’s URL with curl
  2. Place a plist in ~/Library/LaunchAgents and set the run times
  3. Load the job with launchctl
  4. Run it once manually and confirm success in the Worker’s logs

The script is basically a single curl command.

curl -fsS --max-time 600 \
  -H "Authorization: Bearer ${RUN_TOKEN}" \
  https://example.workers.dev/run/location-sync

launchd runs on the Mac’s local time. Not having to convert to UTC like with Cron Triggers is a small but real convenience.

Things to watch when calling from outside

Triggering from outside comes with considerations that Cron Triggers do not have. Here they are, including the ones I actually tripped over.

Warning

Anyone who knows the URL can call it. Authenticate with a secret token, and store the token as a secret. It is safer to set the curl timeout longer than the processing time.

The biggest snag for me was the timeout. The job called from the Mac took about 73 seconds when processing 2 days of data at once. I had set the curl timeout to 90 seconds, and it timed out. I have now extended it to 600 seconds.

According to the official docs, there is no hard limit on the execution time of a Worker invoked over HTTP. The catch is that it only lasts while the client stays connected. If the client disconnects, the processing may stop partway. So I think it is better to give the caller’s timeout plenty of room.

The other one is CPU time. On Free, it is up to 10ms per invocation. However, waiting on fetch() or database responses does not count toward CPU time. A job that mostly waits on external APIs may often fit even if it looks long.

Workaround 3: Consolidate into a dispatcher Worker

If several Workers each have their own crons, you can also consolidate them. Only one “dispatcher” Worker holds the schedules.

The dispatcher looks at controller.cron or the invocation time and calls the Worker in charge. I think calling the target over HTTP is the easiest to understand.

The good thing about this approach is that it stays entirely within Cloudflare. It works even if your home PC is asleep. However, if the dispatcher breaks, all your scheduled jobs stop. On Free, you get up to 50 subrequests per invocation, so keep that in mind if you have many targets.

Workaround 4: Move to the paid plan

The most straightforward option is moving to Workers Paid. The pricing page says it starts at $5 per month per account.

The limit expands from 5 to 250. CPU time per Cron invocation also increases, so it becomes easier to run heavier jobs.

Personally, I doubt I will ever use 250 for home automation. If it is only about the slot limit, try (1) through (3) first. If the weight of the processing becomes a problem, then consider paying. I think that order makes sense.

The trade-off I accepted

Moving the trigger to my Mac did cost me something clear. The signage no longer updates unless the Mac is running. There is no longer any path that delivers without going through the Mac.

That said, we only look at the signage when we are at home. And when we are at home, the Mac is usually running too. Under that assumption, I decided I could accept it.

Conversely, I think notifications that need to arrive reliably even while you are out are safer left on Cron Triggers. Drawing the line so that you move things outside starting with “the ones that are fine if they stop” feels important to me.

Common mistakes and pitfalls

These are the points I felt were worth watching after actually doing this.

The third one in particular is easy to overlook. After deploying, it is reassuring to check in the dashboard or the Wrangler output that the schedules were registered.

Who this suits and who it does not

Workaround 2 (moving the trigger outside) clearly suits some people more than others.

Details
Suits you ifYou have a home PC or server that is always on / You have jobs that only need to run while you are home / You want to increase frequency in fine steps
Does not suit you ifYou need it to run reliably while you are out or traveling / Your home PC goes to sleep often / You do not want to manage authentication

If you fall on the “does not suit” side, (1) or (3) may be a better fit. The paid plan is also an option.

What to check before you start

Frequently asked questions

Q. Is the limit of 5 per Worker?

It is 5 for the whole account. It is not 5 per Worker. Even if you split them across multiple Workers, they are counted as a total.

Q. Does the deploy itself fail if I go over the limit?

In my case, the code deploy went through. Only the schedule setup failed. The error code was 10072. Since it looks like a success at first glance, I think it is worth checking the output carefully.

Q. How many times can I put in one expression?

If you join the hour field with commas, you can combine as many times as you like as long as the minute is the same. However, times with different minutes cannot be expressed in the same expression. Also be careful that combining too many makes the routing in the handler hard to read.

Q. Are controller.cron and event.cron different?

The current official docs use controller.cron to tell which one fired. controller is the first argument of the scheduled handler. Older code sometimes uses event.cron. Either way, the idea of looking at the string of the expression that fired is the same.

Q. If I call it from outside, will I stay within the free request limit?

Free allows up to 100,000 requests per day. Calling it about once an hour is only a dozen or so times a day, so there is plenty of room. Just keep in mind that it is counted together with your other usage.

Q. Are there options other than a home PC for the trigger?

You can also call it from an external scheduling service or another cloud’s scheduled execution feature. The ideas about authentication and timeouts are the same in that case. I think choosing a service based on the impact if it stops is a good approach.

Q. Will the paid plan remove my CPU time worries?

On Paid, CPU time per Cron invocation extends to 30 seconds or 15 minutes depending on the interval. That is far more room than the 10ms on Free. However, note that the limit itself does not go away.

Official sources I referred to

Summary

The Cron Triggers limit is 5 for the whole account. If you keep building home automation, you hit it sooner than you might expect.

First, I combined my morning and noon expressions into one. Next, I moved the trigger outside only for the jobs that assumed my Mac was running. Thanks to that, I now have room to spare and have even increased location sync to every hour.

In exchange, the signage now depends on the Mac. I think the best way to decide how much to move outside is whether it would be a problem if it stopped.

By the way, I wrote about what goes on our home signage in my post about having AI read the school contact notebook and showing it on a home display. I also covered the setup that puts our morning information on a single page in my post about auto-generating a morning briefing. Give them a read if you are interested.

While you are reviewing the plumbing behind your automation, why not take a look at your monthly phone bill too?

Try the fee simulator and get 100 points, no sign-up required Reviewing your phone bill too? Check out Rakuten Mobile

Clicking this opens the Rakuten login page. Once you log in, you’ll see the campaign details.

This article contains affiliate advertising. If you sign up for a product or service through a link on this site, we may receive compensation from the partner company. The site operator is also an employee of Rakuten Group and may receive compensation through an employee referral program. The content and opinions here are based on the operator’s own experience and research regardless of any advertising relationship, but please read with the above in mind. See the Disclaimer & Affiliate Disclosure for details. Disclaimer & Affiliate Disclosure

This article includes machine-translated content. Please check the official Rakuten Mobile multilingual page for exact terms.

If you have concerns about Rakuten Mobile coverage or signal quality, you can consult through the official signal improvement request form .

ABOUT THE AUTHOR
まさきん

Rakuten Group employee · Digital Marketer (holds a Financial Planner qualification)

In his early 40s, part of a dual-income household with four kids. Works as a digital marketer at Rakuten Group, while also using his Financial Planner (FP) qualification to focus on household finances and building assets.

View Profile

Related Articles

RELATED
Gemini (Google AI Pro) Family Sharing Covers Up to 5 Family Members: Setup Steps and Scope Using AI

Gemini (Google AI Pro) Family Sharing Covers Up to 5 Family Members: Setup Steps and Scope

Google AI Pro can be shared with up to 5 family members besides the manager. The Gemini app and storage are shared, while YouTube Premium Lite is for the manager only. Flow limits are shared across the family. I went through the official help to sort out the setup steps and how child accounts are handled.

2026.10.13 · 11 min read
3 Reasons Your AI Subscription Billing Date Does Not Match Your Budget: Claude Upgrades Change It Using AI

3 Reasons Your AI Subscription Billing Date Does Not Match Your Budget: Claude Upgrades Change It

If you upgrade Claude to Max mid-month, your billing cycle resets and that day becomes your new billing date. The yen conversion of dollar charges is set when the data reaches your card company, and statements and budgeting apps lag by a few days. Here are 5 steps to decide the month using the receipt email as your source of truth.

2026.10.13 · 12 min read
Turning Unprocessed freee Accounting Transactions into a Monthly Reconciliation Run: 7:30 on the 5th, 3 Categories, and a Cap of 15 Items Handed to a Human Using AI

Turning Unprocessed freee Accounting Transactions into a Monthly Reconciliation Run: 7:30 on the 5th, 3 Categories, and a Cap of 15 Items Handed to a Human

About 18 unprocessed transactions sat untouched in freee Accounting across 6 weekly checks. Here is my design for a reconciliation run on my home Mac at 7:30 a.m. on the 5th of each month, and where I drew the line: one monthly task for the items that can be confirmed, and one task each, capped at 15, for the ones that need a human decision.

2026.10.12 · 13 min read
Switch to Rakuten Mobile, get up to 14,000 points Try the fee simulator and get 100 points, no sign-up required →
Apply Now →