Cloudflare Workers Cron Triggers — A Complete Guide
Cloudflare Workers cron triggers run a Worker on a recurring schedule at the edge — no servers, no orchestrator, fire-and-forget. This guide covers the wrangler configuration, the `scheduled()` handler signature, local testing, limits, and how to pair cron triggers with KV, D1, and R2 for real workloads.
Updated
Wrangler configuration
Add a [triggers] block (or triggers object in wrangler.jsonc) to your Worker’s config:
# wrangler.toml
name = "my-worker"
main = "src/index.js"
compatibility_date = "2026-05-24"
[triggers]
crons = ["*/5 * * * *"]
// wrangler.jsonc
{
"name": "my-worker",
"main": "src/index.js",
"compatibility_date": "2026-05-24",
"triggers": {
"crons": ["*/5 * * * *"]
}
}
Deploy with wrangler deploy. The trigger is registered automatically — no separate “create cron job” step. You can confirm in the dashboard at Workers & Pages → your worker → Triggers.
The cron expression is 5-field cron. Cloudflare supports the standard Unix syntax plus several Quartz-style extensions — L (last), W (nearest weekday), and # (nth weekday) — so 59 23 LW * * (last weekday of the month) is valid here even though it isn’t in plain Unix cron. All times are UTC — there’s no per-worker timezone option.
The scheduled handler
Export a scheduled handler from your Worker:
// src/index.js
export default {
async scheduled(controller, env, ctx) {
// controller.scheduledTime — number, ms since epoch, when this fire was scheduled
// controller.cron — string, the cron pattern that fired
console.log(`Fired ${controller.cron} at ${new Date(controller.scheduledTime).toISOString()}`);
await doWork(env);
},
// Optional — HTTP fetch handler for non-cron requests
async fetch(request, env) {
return new Response("Hello from a worker");
},
};
The handler signature:
controller.scheduledTime— milliseconds since epoch when the fire was scheduled. Useful when you need “what time was this supposed to run” (vs.Date.now()which is when it actually started).controller.cron— the cron pattern that triggered this fire. Use it to branch logic when you have multiple crons.env— your worker’s bindings (KV namespaces, D1 databases, R2 buckets, secrets).ctx.waitUntil(promise)— extends the Worker’s lifetime to wait for the promise. Use for fire-and-forget work you want to complete but don’t need to return from.
TypeScript variant:
// src/index.ts
export interface Env {
MY_KV: KVNamespace;
MY_DB: D1Database;
}
export default {
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
await env.MY_KV.put("last-run", new Date().toISOString());
},
} satisfies ExportedHandler<Env>;
Multiple crons per worker
A single Worker can have multiple cron triggers (the 5-per-account Free / 250-per-account Paid limit is across your whole account, not per Worker):
[triggers]
crons = [
"*/5 * * * *", # every 5 minutes
"0 0 * * *", # daily at midnight UTC
"0 0 * * 1", # weekly on Monday
]
The same scheduled() handler is called for all of them. Use controller.cron to dispatch:
export default {
async scheduled(controller, env, ctx) {
switch (controller.cron) {
case "*/5 * * * *":
await refreshCache(env);
break;
case "0 0 * * *":
await dailyReport(env);
break;
case "0 0 * * 1":
await weeklyCleanup(env);
break;
}
},
};
For more than a handful of distinct schedules, split into separate Workers. The account-wide cap is 5 triggers on Free and 250 on paid, but well before that limit the cognitive load of one big handler with many branches usually outweighs the deployment overhead of splitting.
Local testing
Cron handlers don’t fire on a schedule in wrangler dev. Instead, run the dev server and hit its /cdn-cgi/handler/scheduled route to invoke the handler manually:
wrangler dev
Then trigger the handler with an HTTP request to that path:
curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*+*+*+*+*"
The cron query parameter URL-encodes the cron expression (spaces as +). This invokes scheduled() with controller.cron set to the value you passed. (The older wrangler dev --test-scheduled + /__scheduled route still works if you prefer it.)
You can also override the scheduled time with time (epoch milliseconds):
curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*/5+*+*+*+*&time=1774051200000"
This is the cleanest way to test cron handlers — no waiting for a real schedule to fire, no deploying to production. Wire it into your test suite to assert the handler does what you expect for each cron pattern.
Logging and observability
console.log / console.error in the handler appear in:
- Real-time logs via
wrangler tailwhile the Worker is running - The Cloudflare dashboard under Workers & Pages → your worker → Logs
- Workers Analytics Engine (paid, if you’ve wired it up)
For production scheduled work, ship structured logs to an external system. Cloudflare’s built-in log retention is short (hours, not days). Common patterns:
console.log(JSON.stringify({ event, ...details }))then read viawrangler tail | jqfor ad-hoc inspection- Use the Logpush API to stream logs to S3, R2, or another sink
- Tail-style integration with Datadog, Honeycomb, or Logflare
For metrics, Cloudflare automatically reports per-Worker invocations, errors, and duration. To track per-cron-pattern metrics, use Workers Analytics Engine and emit a data point on each schedule fire with controller.cron as a dimension.
Limits
Cron triggers per account (not per Worker):
- Free plan: 5
- Paid plan: 250
CPU time per Cron Trigger:
- Free plan: 10 ms (same as HTTP-triggered Workers)
- Paid plan: 30 seconds for schedules more frequent than hourly; 15 minutes for hourly-or-longer intervals
Wall-clock duration for a Cron Trigger is capped at 15 minutes — a handler can wait on I/O within that window, but the CPU-time limits above still apply to actual compute.
Schedule precision:
- Minimum granularity: 1 minute
- Cloudflare fires the handler “near” the scheduled time — not to-the-second precision. Expect a few seconds of jitter.
For longer-running scheduled work, the standard pattern is to use the cron trigger to enqueue work into a queue (Cloudflare Queues, SQS, etc.) that’s processed by a separate, longer-running consumer.
Pairing with KV, D1, R2
Cron triggers are most useful when paired with Workers’ other primitives.
Workers KV — distributed lock to prevent overlap:
async scheduled(controller, env, ctx) {
const lockKey = "cron-lock";
const existing = await env.MY_KV.get(lockKey);
if (existing) {
console.log("Previous run still active, skipping");
return;
}
// Lock with 5-minute TTL (slightly longer than expected runtime)
await env.MY_KV.put(lockKey, Date.now().toString(), { expirationTtl: 300 });
try {
await doWork(env);
} finally {
await env.MY_KV.delete(lockKey);
}
}
KV’s eventual consistency means this lock isn’t bulletproof under contention — for strict concurrency control, use D1 with a transactional update.
D1 — periodic database maintenance:
async scheduled(controller, env, ctx) {
// Delete soft-deleted rows older than 30 days
await env.MY_DB.prepare(`
DELETE FROM users WHERE deleted_at IS NOT NULL AND deleted_at < ?
`).bind(Date.now() - 30 * 86400 * 1000).run();
}
R2 — periodic snapshots or backups:
async scheduled(controller, env, ctx) {
const snapshot = await generateSnapshot(env);
const key = `snapshots/${new Date().toISOString()}.json`;
await env.MY_BUCKET.put(key, JSON.stringify(snapshot));
}
The common thread: cron triggers are great for “do something periodically against my stateful primitives.” For external HTTP work (calling third-party APIs, dispatching to other systems), they’re also fine — just add error handling and consider a DLQ pattern using Queues for retries.
Frequently asked questions
What cron syntax do Cloudflare Workers cron triggers use?
How do I test a cron trigger locally?
How many cron triggers can I have?
How long can a cron-triggered Worker run?
How do I avoid the cron firing while a previous run is still going?
Related
Every 5 Minutes
`*/5 * * * *` — a common Workers cron schedule.
PatternEvery Hour
`0 * * * *` — hourly cache refresh, common Workers pattern.
ToolCron Parser
Verify a cron expression before adding it to wrangler.toml.
GuideCron Dialect Comparison
Cloudflare Workers uses standard Unix 5-field cron — see how it compares to others.
GuideHow Cron Expressions Work
Field-by-field reference for the cron syntax.