cloudflare-cron-triggers について
Cron Triggers を設定して Cloudflare Workers を開発するためのガイドです。scheduled ハンドラー、Cron 式、Wrangler の設定、ローカルテストに加え、複数スケジュールや環境バインディング、Workflows との連携を扱います。
主な機能
- 01scheduled ハンドラーと Cron トリガーの設定
- 025 フィールド Cron 式の記述
- 03Wrangler によるローカルテスト
- 04トリガーに応じたタスクの振り分け
- 05定期タスクと Workflows の連携
活用シーン
- 01Worker タスクの時間単位・定期実行
- 02scheduled ハンドラーのローカル検証
- 03タスクごとの実行スケジュール設定
- 04複数ステップのワークフローの定期起動
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jackspace/ claudeskillz --skill cloudflare-cron-triggersインストール後、Claude Code で/cloudflare-cron-triggersを入力して読み込まれたか確認します。
同梱ファイルREADME.mdSKILL.json
詳細
Cloudflare Cron Triggers
ステータス:本番環境で利用可能 ✅ 最終更新日:2025-10-23 依存関係:cloudflare-worker-base(Worker のセットアップ用) 最新バージョン:[email protected]、@cloudflare/[email protected]
クイックスタート(5 分)
1. 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);
},
};
重要な理由:
- ハンドラー名は正確に
scheduledにする必要があります(scheduledHandlerやonScheduledではありません) - default export オブジェクトでエクスポートする必要があります
- ES モジュール形式を使用する必要があります(Service Worker 形式ではありません)
2. Wrangler で Cron Trigger を設定する
wrangler.jsonc:
{
"name": "my-scheduled-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-23",
"triggers": {
"crons": [
"0 * * * *" // Every hour at minute 0
]
}
}
重要:
- Cron 式は 5 つのフィールドを使用します:
minute hour day-of-month month day-of-week - 時刻はすべてUTC のみです(タイムゾーン変換はありません)
- 変更がグローバルに反映されるまで最大 15 分かかります
3. ローカルでテストする
# Enable scheduled testing
npx wrangler dev --test-scheduled
# In another terminal, trigger the scheduled handler
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
# View output in wrangler dev terminal
テストのヒント:
/__scheduledエンドポイントは--test-scheduledフラグを指定した場合のみ利用できます- クエリパラメーターには任意の Cron 式を渡せます
- Python Workers では代わりに
/cdn-cgi/handler/scheduledを使用します
4. デプロイする
npm run deploy
# or
npx wrangler deploy
デプロイ後:
- 変更が反映されるまで最大 15 分かかる場合があります
- ダッシュボードで確認します:Workers & Pages > [Your Worker] > Cron Triggers
- 過去の実行履歴は Logs タブで確認できます
Cron 式の構文
5 フィールド形式
* * * * *
│ │ │ │ │
│ │ │ │ └─── Day of Week (0-6, Sunday=0)
│ │ │ └───── Month (1-12)
│ │ └─────── Day of Month (1-31)
│ └───────── Hour (0-23)
└─────────── Minute (0-59)
特殊文字
| 文字 | 意味 | 例 |
|---|---|---|
* | 毎回 | * * * * * = 毎分 |
, | リスト | 0,30 * * * * = 毎時 :00 と :30 |
- | 範囲 | 0 9-17 * * * = 午前 9 時から午後 5 時まで毎時 |
/ | 間隔 | */15 * * * * = 15 分ごと |
よく使われるパターン
# 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
重要:タイムゾーンは UTC のみ
- Cron Trigger はすべてUTC 時刻で実行されます
- タイムゾーン変換は利用できません
- ローカル時刻を UTC に手動で変換してください
- 例:PST の午前 9 時は UTC の午後 5 時です(夏時間中は翌日)
ScheduledController インターフェース
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
}
プロパティ
controller.cron(string)
この実行をトリガーした Cron 式です。
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
console.log(`Triggered by: ${controller.cron}`);
// Output: "Triggered by: 0 * * * *"
},
};
使用例: 複数の Cron スケジュールを区別します(「複数の Cron Trigger」パターンを参照)。
controller.type(string)
Cron によってトリガーされた実行では、常に "scheduled" を返します。
if (controller.type === 'scheduled') {
// This is a cron-triggered execution
}
controller.scheduledTime(number)
この実行が予定された Unix タイムスタンプ(エポックからのミリ秒)です。
export default {
async scheduled(controller: ScheduledController): Promise<void> {
const scheduledDate = new Date(controller.scheduledTime);
console.log(`Scheduled for: ${scheduledDate.toISOString()}`);
// Output: "Scheduled for: 2025-10-23T15:00:00.000Z"
},
};
注: これは実際の実行時刻ではなく、予定された時刻です。システム負荷により、実際の実行がわずかに遅れる場合があります(通常 1 秒未満)。
実行コンテキスト
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>)
ハンドラーの戻り後も非同期処理の完了を待つよう、実行コンテキストを延長します。
使用例:
- 外部サービスへのログ記録
- アナリティクスのトラッキング
- クリーンアップ処理
- 重要度の低いバックグラウンドタスク
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' }));
},
};
重要: waitUntil() が失敗した場合、最初に失敗したものがダッシュボードのログにステータスとして報告されます。
統合パターン
1. スタンドアロンのスケジュール実行 Worker
最適な用途: スケジュール実行のみの Worker(HTTP リクエストなし)
// src/index.ts
interface Env {
DB: D1Database;
MY_BUCKET: R2Bucket;
}
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log('Running scheduled maintenance...');
// Database cleanup
await env.DB.prepare('DELETE FROM sessions WHERE expires_at < ?')
.bind(Date.now())
.run();
// Generate daily report
const report = await generateDailyReport(env.DB);
// Upload to R2
await env.MY_BUCKET.put(
`reports/${new Date().toISOString().split('T')[0]}.json`,
JSON.stringify(report)
);
console.log('Maintenance complete');
},
};
2. Hono と組み合わせる(Fetch + Scheduled)
最適な用途: HTTP リクエストとスケジュールされたタスクの両方を処理する Worker
// src/index.ts
import { Hono } from 'hono';
interface Env {
DB: D1Database;
}
const app = new Hono<{ Bindings: Env }>();
// Regular HTTP routes
app.get('/', (c) => c.text('Worker is running'));
app.get('/api/stats', async (c) => {
const stats = await c.env.DB.prepare('SELECT COUNT(*) as count FROM users').first();
return c.json(stats);
});
// Export both fetch handler and scheduled handler
export default {
// Handle HTTP requests
fetch: app.fetch,
// Handle cron triggers
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log('Cron triggered:', controller.cron);
// Run scheduled task
await updateCache(env.DB);
// Log completion
ctx.waitUntil(logExecution(controller.scheduledTime));
},
};
このパターンを使う理由:
- 1 つの Worker で両方の用途に対応できます
- 環境バインディングを共有できます
- 管理する Worker の数を減らせます
- コストを削減できます(Worker のサブスクリプションが 1 つ)
3. 複数の Cron Trigger
最適な用途: タスクごとに異なるスケジュールを設定する場合
wrangler.jsonc:
{
"triggers": {
"crons": [
"*/5 * * * *", // Every 5 minutes
"0 */6 * * *", // Every 6 hours
"0 0 * * *" // Daily at midnight UTC
]
}
}
src/index.ts:
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
// Route based on which cron triggered this execution
switch (controller.cron) {
case '*/5 * * * *':
// Every 5 minutes: Check system health
await checkSystemHealth(env);
break;
case '0 */6 * * *':
// Every 6 hours: Sync data from external API
await syncExternalData(env);
break;
case '0 0 * * *':
// Daily at midnight: Generate reports and cleanup
await generateDailyReports(env);
await cleanupOldData(env);
break;
default:
console.warn(`Unknown cron trigger: ${controller.cron}`);
}
},
};
重要:
- Cron 式を完全一致させてください(空白も一致させる必要があります)
- Worker 1 つあたり最大 3 つの Cron Trigger を設定できます(Free プラン)
- Standard/Paid プランではさらに多く設定できます(上限を確認してください)
4. 環境バインディングへのアクセス
スケジュールハンドラーではすべての Worker バインディングを利用できます:
interface Env {
// Databases
DB: D1Database;
// Storage
MY_BUCKET: R2Bucket;
KV_NAMESPACE: KVNamespace;
// AI & Vectors
AI: Ai;
VECTOR_INDEX: VectorizeIndex;
// Queues & Workflows
MY_QUEUE: Queue;
MY_WORKFLOW: Workflow;
// Durable Objects
RATE_LIMITER: DurableObjectNamespace;
// Secrets
API_KEY: string;
}
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
// D1 Database
const users = await env.DB.prepare('SELECT * FROM users WHERE active = 1').all();
// R2 Storage
const file = await env.MY_BUCKET.get('data.json');
// KV Storage
const config = await env.KV_NAMESPACE.get('config', 'json');
// Workers AI
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
prompt: 'Summarize today\'s data',
});
// Send to Queue
await env.MY_QUEUE.send({ type: 'process', data: users.results });
// Trigger Workflow
await env.MY_WORKFLOW.create({ input: { timestamp: Date.now() } });
// Use secrets
await fetch('https://api.example.com/webhook', {
headers: { Authorization: `Bearer ${env.API_KEY}` },
});
},
};
5. ワークフローとの組み合わせ
最適な用途: スケジュールに従ってトリガーされる、複数ステップの長時間実行タスク
wrangler.jsonc:
{
"triggers": {
"crons": ["0 2 * * *"] // Daily at 2am UTC
},
"workflows": [
{
"name": "daily-report-workflow",
"binding": "DAILY_REPORT"
}
]
}
src/index.ts:
interface Env {
DAILY_REPORT: Workflow;
}
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
console.log('Triggering daily report workflow...');
// Trigger workflow with initial state
const instance = await env.DAILY_REPORT.create({
params: {
date: new Date().toISOString().split('T')[0],
reportType: 'daily-summary',
},
});
console.log(`Workflow started: ${instance.id}`);
},
};
ワークフローを使う理由:
- ワークフローは数時間実行できます(cron ハンドラーには CPU 制限があります)
- リトライとエラー処理が組み込まれています
- ステップをまたいで状態が保持されます
- 複雑な複数ステップのプロセスに適しています
リファレンス: Cloudflare Workflows ドキュメント
6. スケジュールハンドラーでのエラー処理
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
try {
// Main task
await performScheduledTask(env);
} catch (error) {
// Log error
console.error('Scheduled task failed:', error);
// Send alert
await sendAlert({
worker: 'my-scheduled-worker',
cron: controller.cron,
error: error.message,
timestamp: new Date(controller.scheduledTime).toISOString(),
});
// Store failure in database
ctx.waitUntil(
env.DB.prepare(
'INSERT INTO cron_failures (cron, error, timestamp) VALUES (?, ?, ?)'
)
.bind(controller.cron, error.message, Date.now())
.run()
);
// Re-throw to mark execution as failed
throw error;
}
},
};
async function sendAlert(details: any): Promise<void> {
await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🚨 Cron job failed: ${details.worker}`,
blocks: [
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Worker:*\n${details.worker}` },
{ type: 'mrkdwn', text: `*Cron:*\n${details.cron}` },
{ type: 'mrkdwn', text: `*Error:*\n${details.error}` },
{ type: 'mrkdwn', text: `*Time:*\n${details.timestamp}` },
],
},
],
}),
});
}
Wrangler の設定
基本設定
{
"name": "my-scheduled-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-23",
"triggers": {
"crons": ["0 * * * *"]
}
}
複数の Cron トリガー
{
"triggers": {
"crons": [
"*/5 * * * *", // Every 5 minutes
"0 */6 * * *", // Every 6 hours
"0 2 * * *", // Daily at 2am UTC
"0 0 * * 1" // Weekly on Monday at midnight UTC
]
}
}
制限:
- 無料プラン:cron スケジュールは最大 3 つ
- 有料プラン:上限が高くなります(現在の上限を確認してください)
環境別の Cron
{
"name": "my-worker",
"main": "src/index.ts",
"env": {
"dev": {
"triggers": {
"crons": ["*/5 * * * *"] // Dev: every 5 minutes for testing
}
},
"staging": {
"triggers": {
"crons": ["*/30 * * * *"] // Staging: every 30 minutes
}
},
"production": {
"triggers": {
"crons": ["0 * * * *"] // Production: hourly
}
}
}
}
特定の環境をデプロイする:
# Deploy to dev
npx wrangler deploy --env dev
# Deploy to production
npx wrangler deploy --env production
すべての Cron トリガーを削除する
{
"triggers": {
"crons": [] // Empty array removes all crons
}
}
デプロイ後、Worker はスケジュールに従って実行されなくなります。
テストと開発
Wrangler でのローカルテスト
# Start dev server with scheduled testing enabled
npx wrangler dev --test-scheduled
これにより、スケジュールハンドラーをトリガーするための /__scheduled エンドポイントが公開されます。
スケジュールハンドラーをトリガーする
# Trigger with default cron (if only one configured)
curl "http://localhost:8787/__scheduled"
# Trigger with specific cron expression
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
# Trigger with URL-encoded cron
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
注: URL ではスペースの代わりに + を使うか、適切に URL エンコードしてください。
ハンドラーの出力を確認する
# Start dev server
npx wrangler dev --test-scheduled
# In another terminal, trigger and watch output
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
出力は wrangler dev ターミナルに表示されます。
[wrangler:inf] GET /__scheduled?cron=0+*+*+*+* 200 OK (45ms)
Cron job executed at: 2025-10-23T15:00:00.000Z
Triggered by cron: 0 * * * *
Scheduled task completed successfully
複数の Cron 式をテストする
# Test hourly cron
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
# Test daily cron
curl "http://localhost:8787/__scheduled?cron=0+0+*+*+*"
# Test weekly cron
curl "http://localhost:8787/__scheduled?cron=0+0+*+*+1"
Python Workers のテスト
# Python Workers use different endpoint
curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*+*+*+*+*"
グリーンコンピューティング
再生可能エネルギーで稼働するデータセンターでのみ、cron トリガーを実行します。
グリーンコンピューティングを有効にする
ダッシュボードから:
- Workers & Pages に移動します
- Account details セクションで Compute Setting を見つけます
- Change をクリックします
- Green Compute を選択します
- Confirm をクリックします
適用対象:
- アカウント内のすべての cron トリガー
- 二酸化炭素排出量を削減します
- 追加料金はかかりません
- 一部のリージョンではわずかな遅延が発生する場合があります
仕組み:
- Cloudflare は cron の実行を、グリーン電力を使用するデータセンターにルーティングします
- 風力、太陽光、水力発電などの再生可能エネルギーを使用します
- 電力購入契約(PPAs)と再生可能エネルギー証書(RECs)によって検証されます
既知の問題の防止
このスキルは、文書化されている 6 件の問題を防止します。
問題 #1:Cron の変更が反映されない
エラー: wrangler.jsonc で Cron トリガーを更新したのに実行されない
出典: Cloudflare Docs - Cron Triggers
発生する理由:
- Cron トリガーの変更がグローバルに反映されるまで、最大 15 分かかります
- Cloudflare ネットワークがエッジノードを更新するのに時間が必要です
- 通常のデプロイのように即座には反映されません
防止策:
- デプロイ後、実行されることを期待する前に 15 分待ちます
- ダッシュボードで確認します:Workers & Pages > [Worker] > Cron Triggers
- トリガーのみを変更する場合は
wrangler triggers deployを使います
# If you only changed triggers (not code), use:
npx wrangler triggers deploy
# Wait 15 minutes, then verify in dashboard
問題 #2:ハンドラーがエクスポートされていない
エラー: Handler does not export a 'scheduled' method
出典: よくあるデプロイエラー
発生する理由:
- ハンドラーの名前が正確に
scheduledになっていません - ハンドラーがデフォルトのエクスポートオブジェクトでエクスポートされていません
- ES モジュール形式ではなく Service Worker 形式を使用しています
防止策:
// ❌ Wrong: Incorrect handler name
export default {
async scheduledHandler(controller, env, ctx) { }
};
// ❌ Wrong: Not in default export
export async function scheduled(controller, env, ctx) { }
// ✅ Correct: Named 'scheduled' in default export
export default {
async scheduled(controller, env, ctx) { }
};
問題 #3:UTC タイムゾーンの混同
エラー: Cron が想定と異なる時刻に実行される
出典: ユーザーの期待と実際の動作の違い
発生する理由:
- すべての Cron トリガーは UTC 時刻のみで実行されます
- タイムゾーン変換は利用できません
- ユーザーが現地のタイムゾーンで実行されると期待しています
防止策:
現地時刻を手動で UTC に変換してください。
// Want to run at 9am PST (UTC-8)?
// 9am PST = 5pm UTC (17:00)
{
"triggers": {
"crons": ["0 17 * * *"] // 9am PST = 5pm UTC
}
}
// Want to run at 6pm EST (UTC-5)?
// 6pm EST = 11pm UTC (23:00)
{
"triggers": {
"crons": ["0 23 * * *"] // 6pm EST = 11pm UTC
}
}
// Remember: DST changes affect conversion!
// PST is UTC-8, PDT is UTC-7
ツール:
Issue #4:無効な Cron 式
エラー: Cron が実行されず、エラーも表示されない
原因: 検証が通知されずに失敗
発生する理由:
- 無効な Cron 構文は通知されずに失敗します
- 検証はデプロイ時に行われますが、明確に分からない場合があります
- よくある間違い:フィールドの順序が誤っている、無効な範囲を指定している
予防策:
# ❌ 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
検証:
- 式の検証には Crontab Guru を使用します
wrangler deployの出力でエラーを確認します--test-scheduledを使ってローカルでテストします
Issue #5:ES モジュール形式の欠落
エラー: Worker must use ES modules format
原因: レガシーな Service Worker 形式
発生する理由:
- スケジュールされたハンドラーには ES モジュール形式が必要です
- 古い Service Worker 形式はサポートされていません
- コードベース内で形式が混在しています
予防策:
// ❌ 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 時間制限の超過
エラー: CPU time limit exceeded
原因: 実行時間の長いスケジュールタスク
発生する理由:
- デフォルトの CPU 制限は 30 秒です
- 実行時間の長いタスクは制限を超過します
- タイムアウトが自動的に延長されることはありません
予防策:
オプション 1:wrangler.jsonc で CPU 制限を増やす
{
"limits": {
"cpu_ms": 300000 // 5 minutes (max for Standard plan)
}
}
オプション 2:実行時間の長いタスクには Workflows を使用する
// 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' },
});
},
};
オプション 3:小さなチャンクに分割する
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' });
}
},
};
必ず行うこと ✅
- 正確なハンドラー名を使用する -
scheduledを指定する必要があります。scheduledHandlerやその派生名は使用できません - ES モジュール形式を使用する -
addEventListenerではなく、デフォルトオブジェクトでエクスポートします - UTC に変換する - すべての Cron 時刻は UTC です。ローカルタイムゾーンから変換してください
- 15 分待つ - Cron の変更が反映されるまで最大 15 分かかります
- まずローカルでテストする -
wrangler dev --test-scheduledを使用します - Cron 構文を検証する - Crontab Guru を使用します
- エラーを適切に処理する - ログを記録し、アラートを送信し、必要に応じて再スローします
- ctx.waitUntil() を使用する - 重要度の低い非同期処理に使用します
- Workflows を検討する - 30 秒を超える CPU 時間が必要なタスクに使用します
- 実行を監視する - ダッシュボードのログを定期的に確認します
絶対に行わないこと ❌
- ローカルタイムゾーンだと思い込まない - すべての Cron は UTC で実行されます
- 6 フィールドの Cron 式を使用しない - Cloudflare は 5 フィールド形式を使用します(秒はありません)
- 即時に反映されると期待しない - 変更が反映されるまで最大 15 分かかります
- Service Worker 形式を使用しない - ES モジュール形式を使用する必要があります
- エラー処理を忘れない - キャッチされないエラーは通知されずに失敗します
- 制限を増やさずに CPU 負荷の高いタスクを実行しない - デフォルトの制限は 30 秒です
- 曜日に 7 を指定しない - 日曜日には 0 を使用します(範囲は 0~6 のみ)
- テストせずにデプロイしない - 必ず先に
--test-scheduledでテストします - 実行ログを無視しない - ダッシュボードで過去の失敗を確認できます
- テスト用のスケジュールをハードコードしない - 環境ごとの設定を使用します
よくある使用例
1. データベースのクリーンアップ
毎日 UTC 午前 2 時:古いレコードを削除
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
// Delete sessions older than 30 days
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
await env.DB.prepare('DELETE FROM sessions WHERE created_at < ?')
.bind(thirtyDaysAgo)
.run();
// Delete soft-deleted users older than 90 days
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
await env.DB.prepare('DELETE FROM users WHERE deleted_at < ?')
.bind(ninetyDaysAgo)
.run();
console.log('Database cleanup completed');
},
};
wrangler.jsonc:
{
"triggers": {
"crons": ["0 2 * * *"] // Daily at 2am UTC
}
}
2. API データの収集
15 分ごと:外部 API からデータを取得
interface Env {
DB: D1Database;
API_KEY: string;
}
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
try {
// Fetch from external API
const response = await fetch('https://api.example.com/v1/data', {
headers: {
Authorization: `Bearer ${env.API_KEY}`,
},
});
const data = await response.json();
// Store in D1
for (const item of data.items) {
await env.DB.prepare(
'INSERT INTO collected_data (id, value, timestamp) VALUES (?, ?, ?)'
)
.bind(item.id, item.value, Date.now())
.run();
}
console.log(`Collected ${data.items.length} items`);
} catch (error) {
console.error('Failed to collect data:', error);
throw error; // Mark execution as failed
}
},
};
wrangler.jsonc:
{
"triggers": {
"crons": ["*/15 * * * *"] // Every 15 minutes
}
}
3. 日次レポートの生成
毎日 UTC 午前 8 時:レポートを生成してメールで送信
export default {
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
// Generate report from database
const report = await generateDailyReport(env.DB);
// Store in R2
const fileName = `reports/${new Date().toISOString().split('T')[0]}.json`;
await env.MY_BUCKET.put(fileName, JSON.stringify(report));
// Send via email
ctx.waitUntil(sendReportEmail(report, env.RESEND_API_KEY));
console.log('Daily report generated and sent');
},
};
async function generateDailyReport(db: D1Database) {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const startOfDay = yesterday.setHours(0, 0, 0, 0);
const endOfDay = yesterday.setHours(23, 59, 59, 999);
const stats = await db
.prepare(`
SELECT
COUNT(*) as total_users,
COUNT(DISTINCT user_id) as active_users,
SUM(revenue) as total_revenue
FROM events
WHERE timestamp BETWEEN ? AND ?
`)
.bind(startOfDay, endOfDay)
.first();
return {
date: yesterday.toISOString().split('T')[0],
stats,
};
}
wrangler.jsonc:
{
"triggers": {
"crons": ["0 8 * * *"] // Daily at 8am UTC
}
}
4. キャッシュのウォームアップ
毎時:人気コンテンツでキャッシュを事前にウォームアップ
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
// Get most popular pages from analytics
const popularPages = await env.DB
.prepare('SELECT url FROM pages ORDER BY views DESC LIMIT 100')
.all();
// Fetch each page to warm cache
const requests = popularPages.results.map((page) =>
fetch(`https://example.com${page.url}`, {
cf: {
cacheTtl: 3600, // Cache for 1 hour
},
})
);
await Promise.all(requests);
console.log(`Warmed cache for ${popularPages.results.length} pages`);
},
};
wrangler.jsonc:
{
"triggers": {
"crons": ["0 * * * *"] // Every hour
}
}
5. 監視とヘルスチェック
5 分ごと:システムの健全性を確認
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
const checks = await Promise.allSettled([
checkDatabaseHealth(env.DB),
checkAPIHealth(),
checkStorageHealth(env.MY_BUCKET),
]);
const failures = checks.filter((check) => check.status === 'rejected');
if (failures.length > 0) {
// Send alert
await sendAlert({
service: 'health-check',
failures: failures.map((f) => f.reason),
timestamp: new Date().toISOString(),
});
}
},
};
async function checkDatabaseHealth(db: D1Database): Promise<void> {
const result = await db.prepare('SELECT 1 as health').first();
if (!result || result.health !== 1) {
throw new Error('Database health check failed');
}
}
async function checkAPIHealth(): Promise<void> {
const response = await fetch('https://api.example.com/health');
if (!response.ok) {
throw new Error(`API health check failed: ${response.status}`);
}
}
async function checkStorageHealth(bucket: R2Bucket): Promise<void> {
const testObject = await bucket.get('health-check.txt');
if (!testObject) {
throw new Error('Storage health check failed');
}
}
wrangler.jsonc:
{
"triggers": {
"crons": ["*/5 * * * *"] // Every 5 minutes
}
}
TypeScript の型
// Scheduled event controller
interface ScheduledController {
readonly cron: string;
readonly type: string;
readonly scheduledTime: number;
}
// Execution context
interface ExecutionContext {
waitUntil(promise: Promise<any>): void;
passThroughOnException(): void;
}
// Scheduled handler
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void>;
}
制限と料金
制限
| 機能 | 無料プラン | 有料プラン |
|---|---|---|
| Worker あたりの Cron トリガー数 | 3 | より多い(ドキュメントを確認) |
| 実行あたりの CPU 時間 | 10 ms(平均) | 30 秒(デフォルト)、5 分(最大) |
| 実時間 | 30 秒 | 15 分 |
| メモリ | 128 MB | 128 MB |
料金
Cron トリガーにはStandard Workers の料金体系が適用されます。
- Workers Paid Plan:月額 $5 が必要です
- リクエスト:100 万リクエストあたり $0.30(無料枠 1,000 万件を超過した場合)
- CPU 時間:100 万 CPU-ms あたり $0.02(無料枠 3,000 万件を超過した場合)
Cron の実行 = 1 リクエスト
例:
- Cron を毎時実行(1 日 24 回)
- 30 日 × 24 回の実行 = 月間 720 回の実行
- 1 回の実行あたりの平均 CPU 時間は 50ms
料金:
- リクエスト:720 件(無料枠 1,000 万件を大きく下回ります)
- CPU 時間:720 × 50ms = 36,000ms(無料枠 3,000 万件未満です)
- 合計:月額 $5(サブスクリプション料金のみ)
高頻度の例:
- Cron を毎分実行(1 日 1,440 回)
- 30 日 × 1,440 回 = 月間 43,200 回の実行
- 無料枠の上限内です
- 合計:月額 $5
トラブルシューティング
問題:Cron が実行されない
考えられる原因:
- 変更がまだ反映されていない(15 分待ってください)
- cron 式が無効です
- ハンドラーが正しくエクスポートされていません
- Worker がデプロイされていません
解決策:
# Re-deploy
npx wrangler deploy
# Wait 15 minutes
# Check dashboard
# Workers & Pages > [Worker] > Cron Triggers
# Check logs
# Workers & Pages > [Worker] > Logs > Real-time Logs
問題:ハンドラーは実行されるが失敗する
考えられる原因:
- ハンドラー内でエラーが捕捉されていません
- CPU 時間の上限を超過しています
- 環境バインディングが不足しています
- ネットワークがタイムアウトしています
解決策:
export default {
async scheduled(controller, env, ctx) {
try {
await yourTask(env);
} catch (error) {
// Log detailed error
console.error('Handler failed:', {
error: error.message,
stack: error.stack,
cron: controller.cron,
time: new Date(controller.scheduledTime),
});
// Send alert
ctx.waitUntil(sendAlert(error));
// Re-throw to mark as failed
throw error;
}
},
};
エラーの詳細は、ダッシュボードでログを確認してください。
問題:実行時刻が正しくない
原因: UTC と現地タイムゾーンの混同
解決策:
希望する現地時刻を UTC に変換してください。
// Want 9am PST (UTC-8)?
// 9am PST = 5pm UTC (17:00)
{
"triggers": {
"crons": ["0 17 * * *"]
}
}
ツール:
- 世界時計コンバーター
- DST の変更(PST と PDT)に注意してください
問題:ローカルテストが動作しない
考えられる原因:
--test-scheduledフラグがありません- エンドポイントが間違っています(
/__scheduledにする必要があります) - Python Worker の場合(
/cdn-cgi/handler/scheduledを使用してください)
解決策:
# Correct: Start with flag
npx wrangler dev --test-scheduled
# In another terminal
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
本番環境へのデプロイ前チェックリスト
Cron トリガーを本番環境にデプロイする前に、以下を確認してください。
- Crontab Guru で cron 式を検証した
- デフォルトエクスポート内のハンドラー名が
scheduledと完全に一致している - ES モジュール形式を使用している(Service Worker ではない)
- 現地タイムゾーンを UTC に変換した
- ログ記録を含むエラー処理を実装した
- 失敗時のアラートを設定した
- 必要に応じて CPU 上限を引き上げた(
limits.cpu_ms) - 環境バインディングをテストした
-
--test-scheduledを使ってローカルでテストした - ステージング環境でデプロイをテストした
- デプロイ後、反映を待つために 15 分待機した
- ダッシュボードのログで実行を確認した
- モニタリングとアラートを設定した
- スケジュールの詳細を記載してドキュメントを更新した
関連ドキュメント
- Cloudflare Cron Triggers:https://developers.cloudflare.com/workers/configuration/cron-triggers/
- Scheduled Handler API:https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/
- Cron Trigger Examples:https://developers.cloudflare.com/workers/examples/cron-trigger/
- Multiple Cron Triggers:https://developers.cloudflare.com/workers/examples/multiple-cron-triggers/
- Wrangler Triggers Command:https://developers.cloudflare.com/workers/wrangler/commands/#triggers
- Workers Pricing:https://developers.cloudflare.com/workers/platform/pricing/
- Workflows Integration:https://developers.cloudflare.com/workflows/
- Crontab Guru(バリデーター):https://crontab.guru/
- タイムゾーンコンバーター:https://www.timeanddate.com/worldclock/converter.html
最終更新日:2025-10-23 バージョン:1.0.0 メンテナー:Jeremy Dawes | [email protected]