DEV Community

Cover image for Scheduling Cloudflare Workers Beyond Cron Triggers
Ronen Cypis
Ronen Cypis

Posted on • Originally published at runhooks.app

Scheduling Cloudflare Workers Beyond Cron Triggers

Cloudflare Workers Cron Triggers let you run code on a schedule without managing infrastructure. You define a cron expression in wrangler.toml, write a scheduled event handler, deploy, and Cloudflare runs it. For simple, low-frequency tasks inside a single worker, this works well.

Then your project grows. You need a sixth scheduled task, or your nightly data sync fails silently and nobody notices for three days, or you want to schedule calls across multiple workers. That's where Cron Triggers start hitting walls.

How Cloudflare Cron Triggers Work

Cron Triggers are configured in your wrangler.toml file and handled by a scheduled event in your worker code:

# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[triggers]
crons = ["0 */6 * * *", "0 0 * * MON"]
Enter fullscreen mode Exit fullscreen mode
// src/index.ts
export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    switch (event.cron) {
      case '0 */6 * * *':
        await syncExternalData(env);
        break;
      case '0 0 * * MON':
        await generateWeeklyReport(env);
        break;
    }
  },
} satisfies ExportedHandler<Env>;
Enter fullscreen mode Exit fullscreen mode

Cloudflare evaluates the cron expressions and invokes your worker's scheduled handler at the specified times. You can use the event.cron property to differentiate between triggers when a single worker has multiple schedules.

This is straightforward for one or two tasks. The problems surface as you add more.

The Limitations of Cron Triggers

Trigger limits per worker

The free plan allows 3 cron triggers per worker. The paid Workers plan ($5/month) increases this to 5 cron triggers per worker. If your project needs 8 scheduled tasks, you must split them across multiple workers — each with its own wrangler.toml, deployment, and codebase. This adds operational overhead for what should be a scheduling concern, not an architecture concern.

No retries on failure

If your scheduled handler throws an error, exceeds the CPU time limit, or fails for any reason, Cloudflare does not retry the execution. The failed run is gone. The next attempt happens at the next scheduled tick — which could be hours or days away depending on the cron expression.

For a task that cleans up temporary files, a missed run is tolerable. For a task that reconciles billing data or sends time-sensitive notifications, a silent failure can have real consequences.

Limited execution visibility

Cron Triggers provide basic invocation metrics through Workers Analytics — you can see that a trigger fired and whether the worker returned successfully. But there's no structured log of what happened inside the execution: how many records were processed, what the response payload was, or how long individual operations took. Getting that level of detail requires wiring up a separate logging pipeline to a service like Logpush or a third-party provider.

No failure alerts

When a cron trigger fails, Cloudflare doesn't send you an email or a webhook. You find out when you check the dashboard, or when a downstream system breaks because the scheduled task stopped running. There's no built-in way to get notified when a scheduled handler starts failing consistently.

CPU time constraints

CPU time limits vary by plan:

  • Free plan: 10ms CPU time per execution
  • Bundled (paid): 50ms CPU time per execution
  • Standard/Unbound (paid): 30 seconds CPU time per execution

On the free plan, 10ms of CPU time is tight for anything beyond trivial operations. Even on the paid Bundled plan, 50ms limits what you can do in a single scheduled execution. The Standard usage model gives 30 seconds, which covers most workloads, but you need to be on the paid plan to access it.

When Cron Triggers Are Enough

Cron Triggers work well when:

  • You have 5 or fewer scheduled tasks within a single worker
  • Tasks are simple and fast — cache purges, KV store updates, lightweight API calls
  • Missed runs are tolerable — the next scheduled tick will catch up
  • You don't need per-execution logs or failure notifications
  • Your workload fits within the CPU time limits of your plan

If your scheduled tasks check these boxes, Cron Triggers are the simplest solution. No external dependencies, no extra services, no additional cost.

When You Need More

The moment you need any of the following, Cron Triggers become insufficient:

  • More than 5 scheduled tasks without splitting across workers
  • Automatic retries when a scheduled handler fails
  • Execution logs with HTTP status, response body, and duration
  • Failure alerts via email or webhook
  • Scheduling across multiple workers or mixing Workers with other services (APIs on Railway, Cloud Functions, Vercel routes)
  • Complex scheduling with timezone awareness and DST handling

The External Scheduler Approach

Every Cloudflare Worker with a fetch handler is an HTTP endpoint. Instead of relying on the scheduled handler, you can expose your task logic through fetch and trigger it externally:

// src/index.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // Verify the request is from your scheduler
    const authHeader = request.headers.get('Authorization');
    if (authHeader !== `Bearer ${env.CRON_SECRET}`) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const url = new URL(request.url);

    if (url.pathname === '/tasks/sync') {
      const result = await syncExternalData(env);
      return Response.json({ task: 'sync', ...result });
    }

    if (url.pathname === '/tasks/report') {
      const result = await generateWeeklyReport(env);
      return Response.json({ task: 'report', ...result });
    }

    return Response.json({ error: 'Not found' }, { status: 404 });
  },
} satisfies ExportedHandler<Env>;
Enter fullscreen mode Exit fullscreen mode

This changes the architecture: your worker becomes an HTTP API with multiple task endpoints, and an external scheduler handles the timing. You get separate URLs for separate tasks, structured JSON responses, and standard HTTP status codes that an external system can act on.

Adding authorization

Protect your worker from unauthorized requests using a secret stored in Workers secrets:

npx wrangler secret put CRON_SECRET
Enter fullscreen mode Exit fullscreen mode

The fetch handler checks the Authorization header against this secret (shown above). Configure the same Bearer <token> header in your external scheduler. For additional security, restrict requests by IP using Cloudflare Access or validate a signed HMAC.

Step-by-Step Setup With Runhooks

Runhooks is a scheduled HTTP execution service. It calls your endpoints on a schedule with retries, logging, and failure alerts. Here's how to schedule a Cloudflare Worker:

1. Deploy your worker with a fetch handler

Use the fetch handler pattern shown above. Each task gets its own path (/tasks/sync, /tasks/report, etc.). Deploy with npx wrangler deploy.

2. Set a secret for authorization

npx wrangler secret put CRON_SECRET
# Enter a strong random string when prompted
Enter fullscreen mode Exit fullscreen mode

3. Create a job in Runhooks

  • Name: "Sync external data"
  • URL: https://my-worker.your-subdomain.workers.dev/tasks/sync
  • Method: GET or POST
  • Headers: Authorization: Bearer <your-cron-secret>
  • Schedule: 0 */6 * * * (every 6 hours)
  • Retries: 3 attempts with exponential backoff

4. Create a second job for the weekly report

  • Name: "Weekly report"
  • URL: https://my-worker.your-subdomain.workers.dev/tasks/report
  • Schedule: 0 0 * * MON (every Monday at midnight)
  • Timezone: your preferred timezone
  • Retries: 3 attempts

Each job runs independently with its own schedule, retry policy, and alerting. No cron trigger limits. No splitting tasks across multiple workers.

What you gain over Cron Triggers

  • No trigger limits — schedule 20 tasks against one worker, or spread them across 10 workers. Each is a separate HTTP job.
  • Automatic retries — if a cold start, network blip, or transient error causes a failure, the next attempt fires within seconds instead of waiting for the next cron tick.
  • Execution logs — every invocation is recorded with HTTP status code, response body, and duration in milliseconds. When your sync processed 2,400 records in 1.8 seconds, you see it.
  • Failure alerts — email or webhook notifications when a job starts failing. You find out in minutes, not days.
  • Timezone-aware scheduling — set a job to run at 9 AM America/New_York and it stays at 9 AM through DST transitions.
  • Cross-service scheduling — schedule a Cloudflare Worker, a Vercel API route, and a Railway endpoint from the same dashboard. Cron Triggers only work within Cloudflare.

Keeping the Scheduled Handler as a Fallback

You don't have to remove your scheduled handler when switching to external scheduling. A worker can have both — the external scheduler handles the primary schedule with retries and logging, while a cron trigger fires as a lower-frequency fallback (say, once daily) to catch anything missed:

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // External scheduler entry point
    const authHeader = request.headers.get('Authorization');
    if (authHeader !== `Bearer ${env.CRON_SECRET}`) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }
    const result = await syncExternalData(env);
    return Response.json(result);
  },

  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    // Fallback: runs even if the external scheduler is down
    await syncExternalData(env);
  },
} satisfies ExportedHandler<Env>;
Enter fullscreen mode Exit fullscreen mode

Your task logic lives in a shared function, so there's no code duplication.

Get Started

Cloudflare Workers are fast and cheap to run — the cron trigger limitations are the main constraint for scheduled workloads, and they're straightforward to work around:

  1. Expose your task logic through a fetch handler with authorization
  2. Try Runhooks and schedule your workers at any frequency
  3. Get retries, execution logs, and failure alerts that Cron Triggers don't provide

Preview your cron expressions with the cron expression visualizer, and compare plans when you need more jobs or longer log retention.

Frequently Asked Questions

How many cron triggers can a Cloudflare Worker have?

On the free plan, each Cloudflare Worker can have up to 3 cron triggers. On the paid Workers plan ($5/month), the limit increases to 5 cron triggers per worker. If you need more scheduled tasks, you must split them across multiple workers or use an external scheduler to trigger your workers via HTTP.

Do Cloudflare Cron Triggers have built-in retries?

No. If a scheduled handler throws an error or times out, Cloudflare does not retry the execution. The failed run is lost and the next attempt happens at the next scheduled tick. For tasks where missed runs are costly — data syncs, billing reconciliation, alert checks — you need an external scheduler that retries on failure.

Can I trigger a Cloudflare Worker on a cron schedule without using Cron Triggers?

Yes. Every Cloudflare Worker with a fetch handler is a standard HTTP endpoint. You can use an external HTTP scheduler like Runhooks to send a request to your Worker's URL on any cron schedule. This bypasses the cron trigger limit entirely and adds retries, logging, and failure alerts.

What are the CPU time limits for Cloudflare Workers Cron Triggers?

On the free plan, each cron trigger execution is limited to 10ms of CPU time. On the paid Bundled plan, the limit is 50ms. If you use the Standard (unbound) usage model, CPU time extends to 30 seconds per execution. These limits apply equally to both scheduled and fetch handlers.


Disclosure: I'm the founder of Runhooks, one of the tools mentioned in this article.

Top comments (0)