Cloudflare Cron Triggers for scheduled Workers execution. Use for periodic tasks, scheduled jobs, or encountering handler not found, invalid cron expression, timezone errors.
SKILL.md
Cloudflare Cron Triggers
Status: Production Ready ✅
Last Updated: 2025-11-25
Dependencies: cloudflare-worker-base (for Worker setup)
Latest Versions: [email protected], @cloudflare/[email protected]
Quick Start (5 Minutes)
1. Add Scheduled Handler to Your Worker
src/index.ts:
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log('Cron job executed at:', new Date(controller.scheduledTime));
console.log('Triggered by cron:', controller.cron);
// Your scheduled task logic here
await doPeriodicTask(env);
},
};
Why this matters:
Handler must be named exactly scheduled (not scheduledHandler or onScheduled)
Must be exported in default export object
Must use ES modules format (not Service Worker format)
Cron expressions use 5 fields: minute hour day-of-month month day-of-week
All times are UTC only (no timezone conversion)
Changes take up to 15 minutes to propagate globally
3. Test Locally
# Enable scheduled testing
bunx wrangler dev --test-scheduled
# In another terminal, trigger the scheduled handler
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
# View output in wrangler dev terminal
Testing tips:
/__scheduled endpoint is only available with --test-scheduled flag
Can pass any cron expression in query parameter
Python Workers use /cdn-cgi/handler/scheduled instead
* * * * *
│ │ │ │ │
│ │ │ │ └─── Day of Week (0-6, Sunday=0)
│ │ │ └───── Month (1-12)
│ │ └─────── Day of Month (1-31)
│ └───────── Hour (0-23)
└─────────── Minute (0-59)
Special Characters
Character
Meaning
Example
*
Every
* * * * * = every minute
,
List
0,30 * * * * = every hour at :00 and :30
-
Range
0 9-17 * * * = every hour from 9am-5pm
/
Step
*/15 * * * * = every 15 minutes
Common Patterns
# Every minute
* * * * *
# Every 5 minutes
*/5 * * * *
# Every 15 minutes
*/15 * * * *
# Every hour at minute 0
0 * * * *
# Every hour at minute 30
30 * * * *
# Every 6 hours
0 */6 * * *
# Every day at midnight (00:00 UTC)
0 0 * * *
# Every day at noon (12:00 UTC)
0 12 * * *
# Every day at 3:30am UTC
30 3 * * *
# Every Monday at 9am UTC
0 9 * * 1
# Every weekday at 9am UTC
0 9 * * 1-5
# Every Sunday at midnight UTC
0 0 * * 0
# First day of every month at midnight UTC
0 0 1 * *
# Twice a day (6am and 6pm UTC)
0 6,18 * * *
# Every 30 minutes during business hours (9am-5pm UTC, weekdays)
*/30 9-17 * * 1-5
CRITICAL: UTC Timezone Only
All cron triggers execute on UTC time
No timezone conversion available
Convert your local time to UTC manually
Example: 9am PST = 5pm UTC (next day during DST)
ScheduledController Interface
interface ScheduledController {
readonly cron: string; // The cron expression that triggered this execution
readonly type: string; // Always "scheduled"
readonly scheduledTime: number; // Unix timestamp (ms) when scheduled
}
Properties
controller.cron (string)
The cron expression that triggered this execution.
Note: This is the scheduled time, not the actual execution time. Due to system load, actual execution may be slightly delayed (usually <1 second).
Execution Context
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext // ← Execution context
): Promise<void> {
// Use ctx.waitUntil() for async operations that should complete
ctx.waitUntil(logToAnalytics(env));
},
};
ctx.waitUntil(promise: Promise<any>)
Extends the execution context to wait for async operations to complete after the handler returns.
Use cases:
Logging to external services
Analytics tracking
Cleanup operations
Non-critical background tasks
export default {
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
// Critical task - must complete before handler exits
await processData(env);
// Non-critical tasks - can complete in background
ctx.waitUntil(sendMetrics(env));
ctx.waitUntil(cleanupOldData(env));
ctx.waitUntil(notifySlack({ message: 'Cron completed' }));
},
};
Important: First waitUntil() that fails will be reported as the status in dashboard logs.
Integration Patterns
6 production-ready cron patterns:
Standalone Worker with Cron - Single scheduled function for background tasks (database cleanup, report generation)
Hono + Cron Combination - HTTP endpoints + scheduled tasks in one Worker, sharing bindings and reducing costs
Multiple Cron Triggers - Different schedules for different tasks using controller.cron to route execution
Accessing Bindings - Use D1, KV, R2, AI, Vectorize, Queues, Workflows, Durable Objects in scheduled functions
Integrating with Workflows - Trigger complex, long-running multi-step workflows on schedule
Error Handling Best Practices - Comprehensive error handling with retry logic, alerting (Slack/email), failure logging, and monitoring
Load references/integration-patterns.md for complete implementations with code examples, configuration details, and best practices.
Wrangler Configuration
Add cron triggers to wrangler.jsonc in the triggers.crons array. Each trigger requires a cron expression. Supports multiple crons (Free: 3 max, Paid: higher limits) and environment-specific configurations for dev/staging/production deployments.
Load references/wrangler-config.md for complete configuration examples including multiple triggers, environment-specific schedules, timezone handling, and removal procedures.
Testing & Development
Test scheduled functions locally using the /__scheduled endpoint by running bunx wrangler dev --test-scheduled, then triggering handlers with curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*" (use + instead of spaces in cron expressions).
Load references/testing-guide.md for complete testing strategies, local development setup, unit testing examples, integration testing patterns, and production monitoring techniques.
Green Compute
Run cron triggers only in data centers powered by renewable energy.
Validation happens at deploy, but may not be obvious
Common mistakes: wrong field order, invalid ranges
Prevention:
# ❌ Wrong: Too many fields (6 fields instead of 5)
"crons": ["0 0 * * * *"] # Has seconds field - not supported
# ❌ Wrong: Invalid minute range
"crons": ["65 * * * *"] # Minute must be 0-59
# ❌ Wrong: Invalid day of week
"crons": ["0 0 * * 7"] # Day of week is 0-6 (use 0 for Sunday)
# ✅ Correct: 5 fields, valid ranges
"crons": ["0 0 * * 0"] # Sunday at midnight UTC
// ❌ Wrong: Service Worker format
addEventListener('scheduled', (event) => {
event.waitUntil(handleScheduled(event));
});
// ✅ Correct: ES modules format
export default {
async scheduled(controller, env, ctx) {
await handleScheduled(controller, env, ctx);
},
};
Issue #6: CPU Time Limits Exceeded
Error:CPU time limit exceeded
Source: Long-running scheduled tasks
Why It Happens:
Default CPU limit: 30 seconds
Long-running tasks exceed limit
No automatic timeout extension
Prevention:
Option 1: Increase CPU limit in wrangler.jsonc
{
"limits": {
"cpu_ms": 300000 // 5 minutes (max for Standard plan)
}
}
Option 2: Use Workflows for long-running tasks
// Instead of long task in cron:
export default {
async scheduled(controller, env, ctx) {
// Trigger Workflow that can run for hours
await env.MY_WORKFLOW.create({
params: { task: 'long-running-job' },
});
},
};
Option 3: Break into smaller chunks
export default {
async scheduled(controller, env, ctx) {
// Process in batches
const batch = await getNextBatch(env.DB);
for (const item of batch) {
await processItem(item);
}
// If more work, send to Queue for next batch
const hasMore = await hasMoreWork(env.DB);
if (hasMore) {
await env.MY_QUEUE.send({ type: 'continue-processing' });
}
},
};
Always Do ✅
Use exact handler name - Must be scheduled, not scheduledHandler or variants
Use ES modules format - Export in default object, not addEventListener
Convert to UTC - All cron times are UTC, convert from local timezone
Wait 15 minutes - Cron changes take up to 15 min to propagate
Test locally first - Use wrangler dev --test-scheduled
Never assume local timezone - All crons run on UTC
Never use 6-field cron expressions - Cloudflare uses 5-field format (no seconds)
Never rely on instant propagation - Changes take up to 15 minutes
Never use Service Worker format - Must use ES modules format
Never forget error handling - Uncaught errors fail silently
Never run CPU-intensive tasks without limit increase - Default 30s limit
Never use day-of-week 7 - Use 0 for Sunday (0-6 range only)
Never deploy without testing - Always test with --test-scheduled first
Never ignore execution logs - Dashboard shows past failures
Never hardcode schedules for testing - Use environment-specific configs
Common Use Cases
Load references/common-patterns.md for 10 real-world cron patterns including database cleanup, API data collection, daily reports generation, cache warming, monitoring & health checks, data synchronization, backup automation, sitemap generation, webhook processing, and scheduled notifications.