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
| Expression | Meaning |
|---|---|
*/5 * * * * | Every 5 minutes |
0 * * * * | Every hour |
0 0 * * * | Daily at midnight UTC |
0 0 * * 1 | Every 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 yourwrangler.jsonctriggers.
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.