mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-20 06:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# Cloudflare Cron Triggers
|
||||
|
||||
Schedule Workers execution using cron expressions. Runs on Cloudflare's global network during underutilized periods.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **UTC-only execution** - All schedules run on UTC time
|
||||
- **5-field cron syntax** - Quartz scheduler extensions (L, W, #)
|
||||
- **Global propagation** - 15min deployment delay
|
||||
- **At-least-once delivery** - Rare duplicate executions possible
|
||||
- **Workflow integration** - Trigger long-running multi-step tasks
|
||||
- **Green Compute** - Optional carbon-aware scheduling during low-carbon periods
|
||||
|
||||
## Cron Syntax
|
||||
|
||||
```
|
||||
┌─────────── minute (0-59)
|
||||
│ ┌───────── hour (0-23)
|
||||
│ │ ┌─────── day of month (1-31)
|
||||
│ │ │ ┌───── month (1-12, JAN-DEC)
|
||||
│ │ │ │ ┌─── day of week (1-7, SUN-SAT, 1=Sunday)
|
||||
* * * * *
|
||||
```
|
||||
|
||||
**Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth)
|
||||
|
||||
## Common Schedules
|
||||
|
||||
```bash
|
||||
*/5 * * * * # Every 5 minutes
|
||||
0 * * * * # Hourly
|
||||
0 2 * * * # Daily 2am UTC (off-peak)
|
||||
0 9 * * MON-FRI # Weekdays 9am UTC
|
||||
0 0 1 * * # Monthly 1st midnight UTC
|
||||
0 9 L * * # Last day of month 9am UTC
|
||||
0 10 * * MON#2 # 2nd Monday 10am UTC
|
||||
*/10 9-17 * * MON-FRI # Every 10min, 9am-5pm weekdays
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
**wrangler.jsonc:**
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-cron-worker",
|
||||
"triggers": {
|
||||
"crons": ["*/5 * * * *", "0 2 * * *"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Handler:**
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(
|
||||
controller: ScheduledController,
|
||||
env: Env,
|
||||
ctx: ExecutionContext,
|
||||
): Promise<void> {
|
||||
console.log("Cron:", controller.cron);
|
||||
console.log("Time:", new Date(controller.scheduledTime));
|
||||
|
||||
ctx.waitUntil(asyncTask(env)); // Non-blocking
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Test locally:**
|
||||
```bash
|
||||
npx wrangler dev
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
- **Free:** 3 triggers/worker, 10ms CPU
|
||||
- **Paid:** Unlimited triggers, 50ms CPU
|
||||
- **Propagation:** 15min global deployment
|
||||
- **Timezone:** UTC only
|
||||
|
||||
## Reading Order
|
||||
|
||||
**New to cron triggers?** Start here:
|
||||
1. This README - Overview and quick start
|
||||
2. [configuration.md](./configuration.md) - Set up your first cron trigger
|
||||
3. [api.md](./api.md) - Understand the handler API
|
||||
4. [patterns.md](./patterns.md) - Common use cases and examples
|
||||
|
||||
**Troubleshooting?** Jump to [gotchas.md](./gotchas.md)
|
||||
|
||||
## In This Reference
|
||||
- [configuration.md](./configuration.md) - wrangler config, env-specific schedules, Green Compute
|
||||
- [api.md](./api.md) - ScheduledController, noRetry(), waitUntil, testing patterns
|
||||
- [patterns.md](./patterns.md) - Use cases, monitoring, queue integration, Durable Objects
|
||||
- [gotchas.md](./gotchas.md) - Timezone issues, idempotency, security, testing
|
||||
|
||||
## See Also
|
||||
- [workflows](../workflows/) - Alternative for long-running scheduled tasks
|
||||
- [workers](../workers/) - Worker runtime documentation
|
||||
196
.agents/skills/cloudflare-deploy/references/cron-triggers/api.md
Normal file
196
.agents/skills/cloudflare-deploy/references/cron-triggers/api.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Cron Triggers API
|
||||
|
||||
## Basic Handler
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||
console.log("Cron executed:", new Date(controller.scheduledTime));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**JavaScript:** Same signature without types
|
||||
**Python:** `class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx)`
|
||||
|
||||
## ScheduledController
|
||||
|
||||
```typescript
|
||||
interface ScheduledController {
|
||||
scheduledTime: number; // Unix ms when scheduled to run
|
||||
cron: string; // Expression that triggered (e.g., "*/5 * * * *")
|
||||
type: string; // Always "scheduled"
|
||||
noRetry(): void; // Prevent automatic retry on failure
|
||||
}
|
||||
```
|
||||
|
||||
**Prevent retry on failure:**
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
try {
|
||||
await riskyOperation(env);
|
||||
} catch (error) {
|
||||
// Don't retry - failure is expected/acceptable
|
||||
controller.noRetry();
|
||||
console.error("Operation failed, not retrying:", error);
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**When to use noRetry():**
|
||||
- External API failures outside your control (avoid hammering failed services)
|
||||
- Rate limit errors (retry would fail again immediately)
|
||||
- Duplicate execution detected (idempotency check failed)
|
||||
- Non-critical operations where skip is acceptable (analytics, caching)
|
||||
- Validation errors that won't resolve on retry
|
||||
|
||||
## Handler Parameters
|
||||
|
||||
**`controller: ScheduledController`**
|
||||
- Access cron expression and scheduled time
|
||||
|
||||
**`env: Env`**
|
||||
- All bindings: KV, R2, D1, secrets, service bindings
|
||||
|
||||
**`ctx: ExecutionContext`**
|
||||
- `ctx.waitUntil(promise)` - Extend execution for async tasks (logging, cleanup, external APIs)
|
||||
- First `waitUntil` failure recorded in Cron Events
|
||||
|
||||
## Multiple Schedules
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
switch (controller.cron) {
|
||||
case "*/3 * * * *": ctx.waitUntil(updateRecentData(env)); break;
|
||||
case "0 * * * *": ctx.waitUntil(processHourlyAggregation(env)); break;
|
||||
case "0 2 * * *": ctx.waitUntil(performDailyMaintenance(env)); break;
|
||||
default: console.warn(`Unhandled: ${controller.cron}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## ctx.waitUntil Usage
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const data = await fetchCriticalData(); // Critical path
|
||||
|
||||
// Non-blocking background tasks
|
||||
ctx.waitUntil(Promise.all([
|
||||
logToAnalytics(data),
|
||||
cleanupOldRecords(env.DB),
|
||||
notifyWebhook(env.WEBHOOK_URL, data),
|
||||
]));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
```typescript
|
||||
import { WorkflowEntrypoint } from "cloudflare:workers";
|
||||
|
||||
export class DataProcessingWorkflow extends WorkflowEntrypoint {
|
||||
async run(event, step) {
|
||||
const data = await step.do("fetch-data", () => fetchLargeDataset());
|
||||
const processed = await step.do("process-data", () => processDataset(data));
|
||||
await step.do("store-results", () => storeResults(processed));
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const instance = await env.MY_WORKFLOW.create({
|
||||
params: { scheduledTime: controller.scheduledTime, cron: controller.cron },
|
||||
});
|
||||
console.log(`Started workflow: ${instance.id}`);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Testing Handler
|
||||
|
||||
**Local development (/__scheduled endpoint):**
|
||||
```bash
|
||||
# Start dev server
|
||||
npx wrangler dev
|
||||
|
||||
# Trigger any cron
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
|
||||
|
||||
# Trigger specific cron with custom time
|
||||
curl "http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000"
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
- `cron` - Required. URL-encoded cron expression (use `+` for spaces)
|
||||
- `scheduledTime` - Optional. Unix timestamp in milliseconds (defaults to current time)
|
||||
|
||||
**Production security:** The `/__scheduled` endpoint is available in production and can be triggered by anyone. Block it or implement authentication - see [gotchas.md](./gotchas.md#security-concerns)
|
||||
|
||||
**Unit testing (Vitest):**
|
||||
```typescript
|
||||
// test/scheduled.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { env } from "cloudflare:test";
|
||||
import worker from "../src/index";
|
||||
|
||||
describe("Scheduled Handler", () => {
|
||||
it("processes scheduled event", async () => {
|
||||
const controller = { scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled" as const, noRetry: () => {} };
|
||||
const ctx = { waitUntil: (p: Promise<any>) => p, passThroughOnException: () => {} };
|
||||
await worker.scheduled(controller, env, ctx);
|
||||
expect(await env.MY_KV.get("last_run")).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles multiple crons", async () => {
|
||||
const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };
|
||||
await worker.scheduled({ scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled", noRetry: () => {} }, env, ctx);
|
||||
expect(await env.MY_KV.get("last_type")).toBe("frequent");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Automatic retries:**
|
||||
- Failed cron executions are retried automatically unless `noRetry()` is called
|
||||
- Retry happens after a delay (typically minutes)
|
||||
- Only first `waitUntil()` failure is recorded in Cron Events
|
||||
|
||||
**Best practices:**
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
try {
|
||||
await criticalOperation(env);
|
||||
} catch (error) {
|
||||
// Log error details
|
||||
console.error("Cron failed:", {
|
||||
cron: controller.cron,
|
||||
scheduledTime: controller.scheduledTime,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
|
||||
// Decide: retry or skip
|
||||
if (error.message.includes("rate limit")) {
|
||||
controller.noRetry(); // Skip retry for rate limits
|
||||
}
|
||||
// Otherwise allow automatic retry
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](./README.md) - Overview
|
||||
- [patterns.md](./patterns.md) - Use cases, examples
|
||||
- [gotchas.md](./gotchas.md) - Common errors, testing issues
|
||||
@@ -0,0 +1,180 @@
|
||||
# Cron Triggers Configuration
|
||||
|
||||
## wrangler.jsonc
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "my-cron-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01", // Use current date for new projects
|
||||
|
||||
"triggers": {
|
||||
"crons": [
|
||||
"*/5 * * * *", // Every 5 minutes
|
||||
"0 */2 * * *", // Every 2 hours
|
||||
"0 9 * * MON-FRI", // Weekdays at 9am UTC
|
||||
"0 2 1 * *" // Monthly on 1st at 2am UTC
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Green Compute (Beta)
|
||||
|
||||
Schedule crons during low-carbon periods for carbon-aware execution:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "eco-cron-worker",
|
||||
"triggers": {
|
||||
"crons": ["0 2 * * *"]
|
||||
},
|
||||
"placement": {
|
||||
"mode": "smart" // Runs during low-carbon periods
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Modes:**
|
||||
- `"smart"` - Carbon-aware scheduling (may delay up to 24h for optimal window)
|
||||
- Default (no placement config) - Standard scheduling (no delay)
|
||||
|
||||
**How it works:**
|
||||
- Cloudflare delays execution until grid carbon intensity is lower
|
||||
- Maximum delay: 24 hours from scheduled time
|
||||
- Ideal for batch jobs with flexible timing requirements
|
||||
|
||||
**Use cases:**
|
||||
- Nightly data processing and ETL pipelines
|
||||
- Weekly/monthly report generation
|
||||
- Database backups and maintenance
|
||||
- Analytics aggregation
|
||||
- ML model training
|
||||
|
||||
**Not suitable for:**
|
||||
- Time-sensitive operations (SLA requirements)
|
||||
- User-facing features requiring immediate execution
|
||||
- Real-time monitoring and alerting
|
||||
- Compliance tasks with strict time windows
|
||||
|
||||
## Environment-Specific Schedules
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-cron-worker",
|
||||
"triggers": {
|
||||
"crons": ["0 */6 * * *"] // Prod: every 6 hours
|
||||
},
|
||||
"env": {
|
||||
"staging": {
|
||||
"triggers": {
|
||||
"crons": ["*/15 * * * *"] // Staging: every 15min
|
||||
}
|
||||
},
|
||||
"dev": {
|
||||
"triggers": {
|
||||
"crons": ["*/5 * * * *"] // Dev: every 5min
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule Format
|
||||
|
||||
**Structure:** `minute hour day-of-month month day-of-week`
|
||||
|
||||
**Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth)
|
||||
|
||||
## Managing Triggers
|
||||
|
||||
**Remove all:** `"triggers": { "crons": [] }`
|
||||
**Preserve existing:** Omit `"triggers"` field entirely
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# Deploy with config crons
|
||||
npx wrangler deploy
|
||||
|
||||
# Deploy specific environment
|
||||
npx wrangler deploy --env production
|
||||
|
||||
# View deployments
|
||||
npx wrangler deployments list
|
||||
```
|
||||
|
||||
**⚠️ Changes take up to 15 minutes to propagate globally**
|
||||
|
||||
## API Management
|
||||
|
||||
**Get triggers:**
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
|
||||
-H "Authorization: Bearer {api_token}"
|
||||
```
|
||||
|
||||
**Update triggers:**
|
||||
```bash
|
||||
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
|
||||
-H "Authorization: Bearer {api_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"crons": ["*/5 * * * *", "0 2 * * *"]}'
|
||||
```
|
||||
|
||||
**Delete all:**
|
||||
```bash
|
||||
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
|
||||
-H "Authorization: Bearer {api_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"crons": []}'
|
||||
```
|
||||
|
||||
## Combining Multiple Workers
|
||||
|
||||
For complex schedules, use multiple workers:
|
||||
|
||||
```jsonc
|
||||
// worker-frequent.jsonc
|
||||
{
|
||||
"name": "data-sync-frequent",
|
||||
"triggers": { "crons": ["*/5 * * * *"] }
|
||||
}
|
||||
|
||||
// worker-daily.jsonc
|
||||
{
|
||||
"name": "reports-daily",
|
||||
"triggers": { "crons": ["0 2 * * *"] },
|
||||
"placement": { "mode": "smart" }
|
||||
}
|
||||
|
||||
// worker-weekly.jsonc
|
||||
{
|
||||
"name": "cleanup-weekly",
|
||||
"triggers": { "crons": ["0 3 * * SUN"] }
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Separate CPU limits per worker
|
||||
- Independent error isolation
|
||||
- Different Green Compute policies
|
||||
- Easier to maintain and debug
|
||||
|
||||
## Validation
|
||||
|
||||
**Test cron syntax:**
|
||||
- [crontab.guru](https://crontab.guru/) - Interactive validator
|
||||
- Wrangler validates on deploy but won't catch logic errors
|
||||
|
||||
**Common mistakes:**
|
||||
- `0 0 * * *` runs daily at midnight UTC, not your local timezone
|
||||
- `*/60 * * * *` is invalid (use `0 * * * *` for hourly)
|
||||
- `0 2 31 * *` only runs on months with 31 days
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](./README.md) - Overview, quick start
|
||||
- [api.md](./api.md) - Handler implementation
|
||||
- [patterns.md](./patterns.md) - Multi-cron routing examples
|
||||
@@ -0,0 +1,199 @@
|
||||
# Cron Triggers Gotchas
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Timezone Issues"
|
||||
|
||||
**Problem:** Cron runs at wrong time relative to local timezone
|
||||
**Cause:** All crons execute in UTC, no local timezone support
|
||||
**Solution:** Convert local time to UTC manually
|
||||
|
||||
**Conversion formula:** `utcHour = (localHour - utcOffset + 24) % 24`
|
||||
|
||||
**Examples:**
|
||||
- 9am PST (UTC-8) → `(9 - (-8) + 24) % 24 = 17` → `0 17 * * *`
|
||||
- 2am EST (UTC-5) → `(2 - (-5) + 24) % 24 = 7` → `0 7 * * *`
|
||||
- 6pm JST (UTC+9) → `(18 - 9 + 24) % 24 = 33 % 24 = 9` → `0 9 * * *`
|
||||
|
||||
**Daylight Saving Time:** Adjust manually when DST changes, or schedule at times unaffected by DST (e.g., 2am-4am local time usually safe)
|
||||
|
||||
### "Cron Not Executing"
|
||||
|
||||
**Cause:** Missing `scheduled()` export, invalid syntax, propagation delay (<15min), or plan limits
|
||||
**Solution:** Verify export exists, validate at crontab.guru, wait 15+ min after deploy, check plan limits
|
||||
|
||||
### "Duplicate Executions"
|
||||
|
||||
**Cause:** At-least-once delivery
|
||||
**Solution:** Track execution IDs in KV - see idempotency pattern below
|
||||
|
||||
### "Execution Failures"
|
||||
|
||||
**Cause:** CPU exceeded, unhandled exceptions, network timeouts, binding errors
|
||||
**Solution:** Use try-catch, AbortController timeouts, `ctx.waitUntil()` for long ops, or Workflows for heavy tasks
|
||||
|
||||
### "Local Testing Not Working"
|
||||
|
||||
**Problem:** `/__scheduled` endpoint returns 404 or doesn't trigger handler
|
||||
**Cause:** Missing `scheduled()` export, wrangler not running, or incorrect endpoint format
|
||||
**Solution:**
|
||||
|
||||
1. Verify `scheduled()` is exported:
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
console.log("Cron triggered");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
2. Start dev server:
|
||||
```bash
|
||||
npx wrangler dev
|
||||
```
|
||||
|
||||
3. Use correct endpoint format (URL-encode spaces as `+`):
|
||||
```bash
|
||||
# Correct
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
|
||||
|
||||
# Wrong (will fail)
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5 * * * *"
|
||||
```
|
||||
|
||||
4. Update Wrangler if outdated:
|
||||
```bash
|
||||
npm install -g wrangler@latest
|
||||
```
|
||||
|
||||
### "waitUntil() Tasks Not Completing"
|
||||
|
||||
**Problem:** Background tasks in `ctx.waitUntil()` fail silently or don't execute
|
||||
**Cause:** Promises rejected without error handling, or handler returns before promise settles
|
||||
**Solution:** Always await or handle errors in waitUntil promises:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
// BAD: Silent failures
|
||||
ctx.waitUntil(riskyOperation());
|
||||
|
||||
// GOOD: Explicit error handling
|
||||
ctx.waitUntil(
|
||||
riskyOperation().catch(err => {
|
||||
console.error("Background task failed:", err);
|
||||
return logError(err, env);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### "Idempotency Issues"
|
||||
|
||||
**Problem:** At-least-once delivery causes duplicate side effects (double charges, duplicate emails)
|
||||
**Cause:** No deduplication mechanism
|
||||
**Solution:** Use KV to track execution IDs:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const executionId = `${controller.cron}-${controller.scheduledTime}`;
|
||||
const existing = await env.EXECUTIONS.get(executionId);
|
||||
|
||||
if (existing) {
|
||||
console.log("Already executed, skipping");
|
||||
controller.noRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
await env.EXECUTIONS.put(executionId, "1", { expirationTtl: 86400 }); // 24h TTL
|
||||
await performIdempotentOperation(env);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### "Security Concerns"
|
||||
|
||||
**Problem:** `__scheduled` endpoint exposed in production allows unauthorized cron triggering
|
||||
**Cause:** Testing endpoint available in deployed Workers
|
||||
**Solution:** Block `__scheduled` in production:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Block __scheduled in production
|
||||
if (url.pathname === "/__scheduled" && env.ENVIRONMENT === "production") {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
return handleRequest(request, env, ctx);
|
||||
},
|
||||
|
||||
async scheduled(controller, env, ctx) {
|
||||
// Your cron logic
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Also:** Use `env.API_KEY` for secrets (never hardcode)
|
||||
|
||||
**Alternative:** Add middleware to verify request origin:
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === "/__scheduled") {
|
||||
// Check Cloudflare headers to verify internal request
|
||||
const cfRay = request.headers.get("cf-ray");
|
||||
if (!cfRay && env.ENVIRONMENT === "production") {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
return handleRequest(request, env, ctx);
|
||||
},
|
||||
|
||||
async scheduled(controller, env, ctx) {
|
||||
// Your cron logic
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Limits & Quotas
|
||||
|
||||
| Limit | Free | Paid | Notes |
|
||||
|-------|------|------|-------|
|
||||
| Triggers per Worker | 3 | Unlimited | Maximum cron schedules per Worker |
|
||||
| CPU time | 10ms | 50ms | May need `ctx.waitUntil()` or Workflows |
|
||||
| Execution guarantee | At-least-once | At-least-once | Duplicates possible - use idempotency |
|
||||
| Propagation delay | Up to 15 minutes | Up to 15 minutes | Time for changes to take effect globally |
|
||||
| Min interval | 1 minute | 1 minute | Cannot schedule more frequently |
|
||||
| Cron accuracy | ±1 minute | ±1 minute | Execution may drift slightly |
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
**Unit tests:**
|
||||
- Mock `ScheduledController`, `ExecutionContext`, and bindings
|
||||
- Test each cron expression separately
|
||||
- Verify `noRetry()` is called when expected
|
||||
- Use Vitest with `@cloudflare/vitest-pool-workers` for realistic env
|
||||
|
||||
**Integration tests:**
|
||||
- Test via `/__scheduled` endpoint in dev environment
|
||||
- Verify idempotency logic with duplicate `scheduledTime` values
|
||||
- Test error handling and retry behavior
|
||||
|
||||
**Production:** Start with long intervals (`*/30 * * * *`), monitor Cron Events for 24h, set up alerts before reducing interval
|
||||
|
||||
## Resources
|
||||
|
||||
- [Cron Triggers Docs](https://developers.cloudflare.com/workers/configuration/cron-triggers/)
|
||||
- [Scheduled Handler API](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/)
|
||||
- [Cloudflare Workflows](https://developers.cloudflare.com/workflows/)
|
||||
- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)
|
||||
- [Crontab Guru](https://crontab.guru/) - Validator
|
||||
- [Vitest Pool Workers](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples)
|
||||
@@ -0,0 +1,190 @@
|
||||
# Cron Triggers Patterns
|
||||
|
||||
## API Data Sync
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const response = await fetch("https://api.example.com/data", {headers: { "Authorization": `Bearer ${env.API_KEY}` }});
|
||||
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
||||
ctx.waitUntil(env.MY_KV.put("cached_data", JSON.stringify(await response.json()), {expirationTtl: 3600}));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Database Cleanup
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const result = await env.DB.prepare(`DELETE FROM sessions WHERE expires_at < datetime('now')`).run();
|
||||
console.log(`Deleted ${result.meta.changes} expired sessions`);
|
||||
ctx.waitUntil(env.DB.prepare("VACUUM").run());
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Report Generation
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const startOfWeek = new Date(); startOfWeek.setDate(startOfWeek.getDate() - 7);
|
||||
const { results } = await env.DB.prepare(`SELECT date, revenue, orders FROM daily_stats WHERE date >= ? ORDER BY date`).bind(startOfWeek.toISOString()).all();
|
||||
const report = {period: "weekly", totalRevenue: results.reduce((sum, d) => sum + d.revenue, 0), totalOrders: results.reduce((sum, d) => sum + d.orders, 0), dailyBreakdown: results};
|
||||
const reportKey = `reports/weekly-${Date.now()}.json`;
|
||||
await env.REPORTS_BUCKET.put(reportKey, JSON.stringify(report));
|
||||
ctx.waitUntil(env.SEND_EMAIL.fetch("https://example.com/send", {method: "POST", body: JSON.stringify({to: "team@example.com", subject: "Weekly Report", reportUrl: `https://reports.example.com/${reportKey}`})}));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const services = [{name: "API", url: "https://api.example.com/health"}, {name: "CDN", url: "https://cdn.example.com/health"}];
|
||||
const checks = await Promise.all(services.map(async (service) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const response = await fetch(service.url, { signal: AbortSignal.timeout(5000) });
|
||||
return {name: service.name, status: response.ok ? "up" : "down", responseTime: Date.now() - start};
|
||||
} catch (error) {
|
||||
return {name: service.name, status: "down", responseTime: Date.now() - start, error: error.message};
|
||||
}
|
||||
}));
|
||||
ctx.waitUntil(env.STATUS_KV.put("health_status", JSON.stringify(checks)));
|
||||
const failures = checks.filter(c => c.status === "down");
|
||||
if (failures.length > 0) ctx.waitUntil(fetch(env.ALERT_WEBHOOK, {method: "POST", body: JSON.stringify({text: `${failures.length} service(s) down: ${failures.map(f => f.name).join(", ")}`})}));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Batch Processing (Rate-Limited)
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const queueData = await env.QUEUE_KV.get("pending_items", "json");
|
||||
if (!queueData || queueData.length === 0) return;
|
||||
const batch = queueData.slice(0, 100);
|
||||
const results = await Promise.allSettled(batch.map(item => fetch("https://api.example.com/process", {method: "POST", headers: {"Authorization": `Bearer ${env.API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify(item)})));
|
||||
console.log(`Processed ${results.filter(r => r.status === "fulfilled").length}/${batch.length} items`);
|
||||
ctx.waitUntil(env.QUEUE_KV.put("pending_items", JSON.stringify(queueData.slice(100))));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Queue Integration
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const batch = await env.MY_QUEUE.receive({ batchSize: 100 });
|
||||
const results = await Promise.allSettled(batch.messages.map(async (msg) => {
|
||||
await processMessage(msg.body, env);
|
||||
await msg.ack();
|
||||
}));
|
||||
console.log(`Processed ${results.filter(r => r.status === "fulfilled").length}/${batch.messages.length}`);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const startTime = Date.now();
|
||||
const meta = { cron: controller.cron, scheduledTime: controller.scheduledTime };
|
||||
console.log("[START]", meta);
|
||||
try {
|
||||
const result = await performTask(env);
|
||||
console.log("[SUCCESS]", { ...meta, duration: Date.now() - startTime, count: result.count });
|
||||
ctx.waitUntil(env.METRICS.put(`cron:${controller.scheduledTime}`, JSON.stringify({ ...meta, status: "success" }), { expirationTtl: 2592000 }));
|
||||
} catch (error) {
|
||||
console.error("[ERROR]", { ...meta, duration: Date.now() - startTime, error: error.message });
|
||||
ctx.waitUntil(fetch(env.ALERT_WEBHOOK, { method: "POST", body: JSON.stringify({ text: `Cron failed: ${controller.cron}`, error: error.message }) }));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**View logs:** `npx wrangler tail` or Dashboard → Workers & Pages → Worker → Logs
|
||||
|
||||
## Durable Objects Coordination
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const stub = env.COORDINATOR.get(env.COORDINATOR.idFromName("cron-lock"));
|
||||
const acquired = await stub.tryAcquireLock(controller.scheduledTime);
|
||||
if (!acquired) {
|
||||
controller.noRetry();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await performTask(env);
|
||||
} finally {
|
||||
await stub.releaseLock();
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Python Handler
|
||||
|
||||
```python
|
||||
from workers import WorkerEntrypoint
|
||||
|
||||
class Default(WorkerEntrypoint):
|
||||
async def scheduled(self, controller, env, ctx):
|
||||
data = await env.MY_KV.get("key")
|
||||
ctx.waitUntil(env.DB.execute("DELETE FROM logs WHERE created_at < datetime('now', '-7 days')"))
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
**Local testing with /__scheduled:**
|
||||
```bash
|
||||
# Start dev server
|
||||
npx wrangler dev
|
||||
|
||||
# Test specific cron
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
|
||||
|
||||
# Test with specific time
|
||||
curl "http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000"
|
||||
```
|
||||
|
||||
**Unit tests:**
|
||||
```typescript
|
||||
// test/scheduled.test.ts
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { env } from "cloudflare:test";
|
||||
import worker from "../src/index";
|
||||
|
||||
describe("Scheduled Handler", () => {
|
||||
it("executes cron", async () => {
|
||||
const controller = { scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled" as const, noRetry: vi.fn() };
|
||||
const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn() };
|
||||
await worker.scheduled(controller, env, ctx);
|
||||
expect(await env.MY_KV.get("last_run")).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls noRetry on duplicate", async () => {
|
||||
const controller = { scheduledTime: 1704067200000, cron: "0 2 * * *", type: "scheduled" as const, noRetry: vi.fn() };
|
||||
await env.EXECUTIONS.put("0 2 * * *-1704067200000", "1");
|
||||
await worker.scheduled(controller, env, { waitUntil: vi.fn(), passThroughOnException: vi.fn() });
|
||||
expect(controller.noRetry).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](./README.md) - Overview
|
||||
- [api.md](./api.md) - Handler implementation
|
||||
- [gotchas.md](./gotchas.md) - Troubleshooting
|
||||
Reference in New Issue
Block a user