Cron Triggers

Run a Worker on a schedule without any incoming HTTP request. The Webhook Hub uses this to health-check forwarding targets every 5 minutes and mark unhealthy ones in D1.

Prerequisites: Workers, D1 CRUD

Configure the Cron Schedule

Add a triggers block to wrangler.jsonc:

{
  "name": "webhook-hub",
  "main": "src/index.ts",
  "compatibility_date": "2025-01-01",
  "compatibility_flags": ["nodejs_compat"],

  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "webhook-hub-db",
      "database_id": "<your-database-id>"
    }
  ],

  "triggers": {
    "crons": ["*/5 * * * *"]
  }
}

This runs the Worker every 5 minutes. You can add multiple schedules:

{
  "triggers": {
    "crons": [
      "*/5 * * * *",
      "0 0 * * *"
    ]
  }
}

Cron Expression Reference

ExpressionMeaning
*/5 * * * *Every 5 minutes
0 * * * *Every hour
0 0 * * *Daily at midnight UTC
0 0 * * 1Every Monday at midnight UTC
0 0 1 * *First day of every month

The format is minute hour day-of-month month day-of-week. All times are UTC.

Implement the Scheduled Handler

Export a scheduled function alongside the Hono fetch handler:

import { Hono } from "hono";

const app = new Hono<{ Bindings: Env }>();

// Regular HTTP routes
app.get("/", (c) => c.text("Webhook Hub"));
app.get("/health", (c) => c.json({ status: "ok" }));

// ... other routes ...

export default {
  fetch: app.fetch,

  async scheduled(
    controller: ScheduledController,
    env: Env,
    ctx: ExecutionContext
  ) {
    switch (controller.cron) {
      case "*/5 * * * *":
        await checkForwardingTargets(env);
        break;
      case "0 0 * * *":
        await cleanupOldWebhooks(env);
        break;
    }
  },
};

The controller.cron string matches the expression from wrangler.jsonc, so you can handle multiple schedules in one Worker.

Health Check Implementation

Check each active forwarding target and update its status:

async function checkForwardingTargets(env: Env): Promise<void> {
  const rules = await env.DB.prepare(
    "SELECT id, target_url FROM forwarding_rules WHERE active = 1"
  ).all<{ id: number; target_url: string }>();

  for (const rule of rules.results) {
    let healthy = false;

    try {
      const response = await fetch(rule.target_url, {
        method: "HEAD",
        signal: AbortSignal.timeout(5000),
      });
      healthy = response.ok;
    } catch {
      healthy = false;
    }

    await env.DB.prepare(
      "UPDATE forwarding_rules SET last_health_check = datetime('now'), healthy = ? WHERE id = ?"
    )
      .bind(healthy ? 1 : 0, rule.id)
      .run();
  }

  console.log(`Health check complete: ${rules.results.length} targets checked`);
}

This requires adding columns to the forwarding_rules table:

ALTER TABLE forwarding_rules ADD COLUMN healthy INTEGER NOT NULL DEFAULT 1;
ALTER TABLE forwarding_rules ADD COLUMN last_health_check TEXT;

Cleanup Old Data

A daily cron to remove webhooks older than 30 days:

async function cleanupOldWebhooks(env: Env): Promise<void> {
  const result = await env.DB.prepare(
    "DELETE FROM webhooks WHERE received_at < datetime('now', '-30 days')"
  ).run();

  console.log(`Cleanup: deleted ${result.meta.changes} old webhooks`);
}

Test Locally

Trigger cron handlers manually during development:

# Start dev server
npx wrangler dev

# In another terminal, trigger the scheduled handler
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"

The __scheduled endpoint is only available in local dev mode. It simulates a cron trigger with the specified expression.

Gotcha: The cron expression in the query string must be URL-encoded. Spaces become + or %20. The expression must match one defined in your wrangler.jsonc triggers.

What You Built

The Webhook Hub now runs periodic health checks on forwarding targets and cleans up old data automatically. Unhealthy targets are flagged in D1, so the delivery consumer can skip them or alert you.

Next: Full-Stack App to build a dashboard for viewing webhooks and managing forwarding rules.