Skip to content

Workers Cron Triggers -- Scheduled Tasks

DodaTech 5 min read

In this tutorial, you will learn how to use Cloudflare Workers Cron Triggers to run code on a schedule without maintaining any server infrastructure. Scheduled tasks are important because many backend operations -- daily report generation, cache warming, database cleanup, and API polling -- must run at specific intervals. A real-world example is an analytics platform that aggregates hourly pageview counts from KV stores and writes summaries to D1 every hour.

How Cron Triggers Work

A Cron Trigger is a Worker that runs on a schedule defined by a cron expression. Instead of listening for HTTP requests, the Worker's scheduled handler is invoked at the specified times. The Worker runs in the same runtime environment as request-driven Workers, with access to all the same bindings (KV, D1, R2, Queues, environment variables). The key difference is that there is no incoming Request object -- instead, the handler receives a ScheduledEvent with a cron string and a scheduledTime timestamp.

Cron Trigger Architecture

flowchart TD
    C[Cron Expression */15 * * * *] --> T[Cloudflare Scheduler]
    T --> W[Worker scheduled handler]
    W --> B[Bindings KV / D1 / R2]
    W --> L[Logging to Tail Worker]
    W --> R[Result stored or sent]

    subgraph Schedule
        M1[Minute]
        H[Hour]
        D[Day]
        M[Month]
        WK[Weekday]
    end

    style C fill:#f90,color:#fff
    style W fill:#3498db,color:#fff

The scheduler invokes the Worker at each matching time. Workers have a maximum execution duration depending on your plan: 30 seconds on the free plan and 15 minutes on the paid plan. Cron Triggers do not retry on failure automatically.

Defining a Cron Trigger

// wrangler.toml
// [triggers]
// crons = ["0 */6 * * *", "0 0 * * 0"]

// worker.js -- scheduled handler
export default {
  async scheduled(event, env, ctx) {
    // event.cron: the cron expression that triggered this run
    // event.scheduledTime: the Unix timestamp of the scheduled time

    const { results } = await env.DB.prepare(
      'SELECT COUNT(*) AS total FROM sessions WHERE expires_at < datetime("now")'
    ).first();

    if (results.total > 0) {
      await env.DB.prepare(
        'DELETE FROM sessions WHERE expires_at < datetime("now")'
      ).run();
    }

    console.log(`Cleaned up ${results.total} expired sessions at ${new Date(event.scheduledTime).toISOString()}`);
  }
};

// Expected output in logs:
// Cleaned up 47 expired sessions at 2026-06-23T06:00:00.000Z

The scheduled handler does not return a Response. Output is written to logs or to external storage. The crons array in wrangler.toml can contain multiple cron expressions.

Email Report Generation

// Daily sales report using D1 + email via API
export default {
  async scheduled(event, env, ctx) {
    const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];

    const { results } = await env.DB.prepare(
      `SELECT product, SUM(amount) AS total, COUNT(*) AS orders
       FROM sales
       WHERE DATE(created_at) = ?1
       GROUP BY product
       ORDER BY total DESC`
    ).bind(yesterday).all();

    const report = {
      date: yesterday,
      totalRevenue: results.reduce((s, r) => s + r.total, 0),
      topProducts: results.slice(0, 5)
    };

    await env.REPORT_BUCKET.put(
      `reports/sales-${yesterday}.json`,
      JSON.stringify(report, null, 2)
    );

    console.log(`Sales report generated: $${report.totalRevenue} revenue on ${yesterday}`);
  }
};

// Expected output in R2 bucket: reports/sales-2026-06-22.json
// Expected log:
// Sales report generated: $12450.50 revenue on 2026-06-22

This Worker runs daily at midnight, aggregates the previous day's sales from D1, and writes a JSON report to R2. The report can be served via public bucket or processed further.

Cache Warming with KV

// Preload popular content into KV every 5 minutes
export default {
  async scheduled(event, env, ctx) {
    const popularPaths = [
      '/api/posts',
      '/api/categories',
      '/api/featured'
    ];

    const results = [];

    for (const path of popularPaths) {
      const start = Date.now();
      const response = await fetch(new Request(`https://api.example.com${path}`));
      const data = await response.text();
      const duration = Date.now() - start;

      // Cache in KV with 1-hour TTL
      await env.CACHE.put(
        `response:${path}`,
        data,
        { expirationTtl: 3600 }
      );

      results.push({ path, status: response.status, duration });
    }

    console.log(`Cache warmed: ${JSON.stringify(results)}`);
  }
};

// Expected output in logs:
// Cache warmed: [{"path":"/api/posts","status":200,"duration":120},{"path":"/api/categories","status":200,"duration":95},{"path":"/api/featured","status":200,"duration":110}]

Cache warming runs on a 5-minute schedule, fetching popular API endpoints and storing the results in KV. When users request these endpoints, the Worker serves the pre-warmed cache instead of hitting the origin.

Common Errors and Troubleshooting

Cron Expression Invalid

Cron expressions must have five fields: minute, hour, day-of-month, month, day-of-week. Six-field expressions (with seconds) are not supported. Validate your cron expression with an online tool before deploying.

Execution Timeout

Workers on the free plan timeout after 30 seconds. Use ctx.waitUntil() to continue background tasks after the handler returns, but the total duration including waitUntil tasks cannot exceed the plan limit.

Scheduled Handler Not Found

If the Worker exports a fetch handler but no scheduled handler, the cron trigger runs with no effect. The Worker module must export a scheduled function for cron triggers to have any purpose.

Duplicate Executions

Cron Triggers guarantee at-least-once delivery. Under rare circumstances, the same trigger may fire twice. Design your scheduled Workers to be idempotent -- running the same job twice should produce the same result as running it once.

Local Testing

Wrangler does not natively trigger cron schedules locally. Use wrangler dev --test-scheduled to manually invoke the scheduled handler with a GET request to __scheduled.

Practice Questions

  1. What function must a Worker export to handle cron triggers?
  2. What is the maximum execution time for a cron-triggered Worker on the free plan?
  3. How do you define cron schedules in wrangler.toml?

FAQ

Can I have multiple cron triggers in one Worker?

Yes. You can define multiple cron expressions in the [triggers] crons array in wrangler.toml. All triggers call the same scheduled handler. Use the event.cron property inside the handler to differentiate behaviour for different schedules.

Do Cron Triggers retry on failure?

No. Cron Triggers do not automatically retry if the Worker fails or times out. For critical jobs, implement your own retry logic using a queue, or use Workers Queues combined with a cron trigger that reads from a dead-letter queue.

Can I use environment variables with Cron Triggers?

Yes. Cron-triggered Workers have full access to environment variables, secrets, and bindings (KV, D1, R2, Queues) just like HTTP-triggered Workers. The env parameter in the scheduled handler provides access to all configured bindings.

Summary

Workers Cron Triggers let you run code on a schedule using standard cron expressions. The scheduled handler receives a ScheduledEvent with the cron expression and scheduled time. Use cron triggers for session cleanup, report generation, cache warming, data synchronization, and any other time-based Background Jobs. Design Workers to be idempotent since triggers may fire more than once. Cron Triggers combined with D1, KV, and R2 enable building complete Serverless Data Pipelines at the edge.

This guide is brought to you by the developers of Cloudflare, Cloudflare Workers, and Durga Antivirus Pro at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro