Monitor Cloudflare Workers Cron Triggers
Updated
Cron Triggers let a Cloudflare Worker run on a schedule without any server. That convenience has a downside:
when a scheduled run throws, exceeds its CPU limit or the trigger was removed by a deploy with a different
config, the only trace is in the Worker's logs. This guide adds Cuckoo pings to the scheduled() handler so
a failed or missing run sends an alert.
How Cuckoo sees the Worker
Each check has a ping URL. The handler calls:
https://cuckoo.metastic.ch/ping/YOUR-UUID/startwhen the run begins,https://cuckoo.metastic.ch/ping/YOUR-UUIDwhen the work succeeded,https://cuckoo.metastic.ch/ping/YOUR-UUID/failwhen it threw.
If no result arrives within the schedule plus grace time, the check goes down and you are notified by email, Slack, Discord or webhook. That covers both a crashing handler and a trigger that no longer fires.
Step 1: configure the Cron Trigger
In wrangler.toml:
name = "nightly-sync"
main = "src/index.js"
compatibility_date = "2026-11-01"
[triggers]
crons = ["30 2 * * *"]
Or in wrangler.jsonc:
{
"name": "nightly-sync",
"main": "src/index.js",
"compatibility_date": "2026-11-01",
"triggers": { "crons": ["30 2 * * *"] }
}
Points to know:
- Cron Triggers always run in UTC.
30 2 * * *is 02:30 UTC, which is 03:30 or 04:30 in Zurich depending on daylight saving time. - A deploy replaces the triggers with what is in the config. Removing the
[triggers]block, or deploying from a branch without it, removes the schedule. - Adding, changing or removing a trigger can take up to 15 minutes to propagate after a deploy.
Step 2: store the ping URL as a secret
npx wrangler secret put CUCKOO_URL
# paste: https://cuckoo.metastic.ch/ping/YOUR-UUID
Secrets are available on env in the handler and are not stored in your repository.
Step 3: the scheduled() handler
async function ping(env, suffix = "", body) {
try {
await fetch(env.CUCKOO_URL + suffix, {
method: body ? "POST" : "GET",
body: body ? String(body).slice(0, 10000) : undefined,
signal: AbortSignal.timeout(5000),
});
} catch (err) {
console.log("heartbeat ping failed:", err.message);
}
}
async function runJob(env, controller) {
// your scheduled work, for example syncing data into KV or D1
}
export default {
async scheduled(controller, env, ctx) {
await ping(env, "/start");
try {
await runJob(env, controller);
} catch (err) {
ctx.waitUntil(ping(env, "/fail", `${controller.cron}: ${err.stack || err}`));
throw err;
}
ctx.waitUntil(ping(env));
},
};
How it works:
controller.cronholds the cron expression that fired andcontroller.scheduledTimethe scheduled time in milliseconds. If you configure several crons in one Worker, usecontroller.cronto pick the job and the matching ping URL.ctx.waitUntil()keeps the Worker alive until the promise settles, even after the handler has returned. That lets the result ping finish without delaying the outcome of the run.- The failure path re-throws the error, so the run is still marked as failed in Cloudflare's dashboard and logs.
ping()catches its own errors, so an unreachable ping URL never turns a good run into a failed one.
Awaiting the start ping keeps the order of events clear. If you prefer not to wait for it, wrap it in
ctx.waitUntil() as well.
Several crons in one Worker
const CHECKS = {
"30 2 * * *": "CUCKOO_URL_NIGHTLY",
"*/15 * * * *": "CUCKOO_URL_SYNC",
};
export default {
async scheduled(controller, env, ctx) {
const url = env[CHECKS[controller.cron]];
// pass url to ping() and run the job selected by controller.cron
},
};
Each cron gets its own check and secret, so alerts tell you which schedule is failing.
Testing locally
npx wrangler dev
# in a second terminal:
curl "http://localhost:8787/cdn-cgi/local/scheduled?cron=30+2+*+*+*"
This calls the handler once with the given cron. Older Wrangler versions used wrangler dev --test-scheduled and the /__scheduled path instead. For local tests, create a separate test check, or use a
.dev.vars file with a different CUCKOO_URL so test runs do not affect the production check.
Schedule and grace advice
In Cuckoo, use the same cron expression with time zone UTC. Scheduled Workers usually start close to the
scheduled minute, so a grace time of a few minutes plus the expected runtime is enough for short jobs. Set max
runtime slightly above the CPU and wall time you expect: a run that is killed for exceeding its limits sends
/start but no result, and max runtime makes the check go down.
Troubleshooting
- No pings, no runs: open the Worker in the dashboard and check Settings → Triggers for the cron. If it
is missing, the last deploy used a config without
[triggers]. - Runs listed, but no pings: check that
CUCKOO_URLis set for the deployed environment withnpx wrangler secret list. Secrets are per environment when you use--env. - Fail pings with "exceeded CPU": split the work into smaller batches, or move it to a Queue or Workflow that the cron only starts.
FAQ
Why use ctx.waitUntil for the ping?
The runtime can end the invocation once the handler returns. ctx.waitUntil() tells it to wait for the ping to
finish, so the request is not cut off and the handler does not need to block on it.
Can Cron Triggers use my local time zone?
No. Cron Triggers are evaluated in UTC. Convert your local time to UTC, and remember that the local time of the run shifts by an hour when daylight saving time starts or ends.
Do I need one Worker per scheduled job?
No. One Worker can have several crons. Use controller.cron to tell them apart and give each its own Cuckoo
check, so one failing job does not hide behind another one's success.
Related guides
- Monitor AWS EventBridge and Lambda Cron JobsMonitor scheduled AWS Lambda functions: EventBridge cron syntax with six fields and ?, time zones, Python and Node handlers that ping start, success and fail.
- Monitor Google Cloud Run Jobs and Cloud SchedulerMonitor Cloud Run jobs triggered by Cloud Scheduler: ping once per execution, handle CLOUD_RUN_TASK_INDEX and max-retries, and avoid false alerts on retries.
- Cron Job Start and Fail SignalsSend /start, success and /fail pings from cron jobs to measure run time and catch hung jobs. Includes a reusable bash wrapper script with trap and exit codes.
- Cron Schedule, Time Zone and Grace PeriodPick the right monitoring schedule for a cron job: period or cron expression, IANA time zones, how cron handles daylight saving time, and sizing grace time.