Curated Skills
by lstudlo

cloudflare

references/cron-triggers/patterns.md

.md 191 lines
Content
# 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