mcpField
C
cloudflare-cron-triggers

cloudflare-cron-triggers

github.com/jackspaceクラウド・DevOps1646 回インストール12 週間前に更新
リポジトリ

cloudflare-cron-triggers について

Cron Triggers を設定して Cloudflare Workers を開発するためのガイドです。scheduled ハンドラー、Cron 式、Wrangler の設定、ローカルテストに加え、複数スケジュールや環境バインディング、Workflows との連携を扱います。

主な機能

  1. 01scheduled ハンドラーと Cron トリガーの設定
  2. 025 フィールド Cron 式の記述
  3. 03Wrangler によるローカルテスト
  4. 04トリガーに応じたタスクの振り分け
  5. 05定期タスクと Workflows の連携

活用シーン

  1. 01Worker タスクの時間単位・定期実行
  2. 02scheduled ハンドラーのローカル検証
  3. 03タスクごとの実行スケジュール設定
  4. 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 トリガーを実行します。

グリーンコンピューティングを有効にする

ダッシュボードから:

  1. Workers & Pages に移動します
  2. Account details セクションで Compute Setting を見つけます
  3. Change をクリックします
  4. Green Compute を選択します
  5. 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' });
    }
  },
};

必ず行うこと ✅

  1. 正確なハンドラー名を使用する - scheduled を指定する必要があります。scheduledHandler やその派生名は使用できません
  2. ES モジュール形式を使用する - addEventListener ではなく、デフォルトオブジェクトでエクスポートします
  3. UTC に変換する - すべての Cron 時刻は UTC です。ローカルタイムゾーンから変換してください
  4. 15 分待つ - Cron の変更が反映されるまで最大 15 分かかります
  5. まずローカルでテストする - wrangler dev --test-scheduled を使用します
  6. Cron 構文を検証する - Crontab Guru を使用します
  7. エラーを適切に処理する - ログを記録し、アラートを送信し、必要に応じて再スローします
  8. ctx.waitUntil() を使用する - 重要度の低い非同期処理に使用します
  9. Workflows を検討する - 30 秒を超える CPU 時間が必要なタスクに使用します
  10. 実行を監視する - ダッシュボードのログを定期的に確認します

絶対に行わないこと ❌

  1. ローカルタイムゾーンだと思い込まない - すべての Cron は UTC で実行されます
  2. 6 フィールドの Cron 式を使用しない - Cloudflare は 5 フィールド形式を使用します(秒はありません)
  3. 即時に反映されると期待しない - 変更が反映されるまで最大 15 分かかります
  4. Service Worker 形式を使用しない - ES モジュール形式を使用する必要があります
  5. エラー処理を忘れない - キャッチされないエラーは通知されずに失敗します
  6. 制限を増やさずに CPU 負荷の高いタスクを実行しない - デフォルトの制限は 30 秒です
  7. 曜日に 7 を指定しない - 日曜日には 0 を使用します(範囲は 0~6 のみ)
  8. テストせずにデプロイしない - 必ず先に --test-scheduled でテストします
  9. 実行ログを無視しない - ダッシュボードで過去の失敗を確認できます
  10. テスト用のスケジュールをハードコードしない - 環境ごとの設定を使用します

よくある使用例

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 MB128 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 が実行されない

考えられる原因:

  1. 変更がまだ反映されていない(15 分待ってください)
  2. cron 式が無効です
  3. ハンドラーが正しくエクスポートされていません
  4. 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

問題:ハンドラーは実行されるが失敗する

考えられる原因:

  1. ハンドラー内でエラーが捕捉されていません
  2. CPU 時間の上限を超過しています
  3. 環境バインディングが不足しています
  4. ネットワークがタイムアウトしています

解決策:

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 * * *"]
  }
}

ツール:


問題:ローカルテストが動作しない

考えられる原因:

  1. --test-scheduled フラグがありません
  2. エンドポイントが間違っています(/__scheduled にする必要があります)
  3. 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 分待機した
  • ダッシュボードのログで実行を確認した
  • モニタリングとアラートを設定した
  • スケジュールの詳細を記載してドキュメントを更新した

関連ドキュメント


最終更新日:2025-10-23 バージョン:1.0.0 メンテナー:Jeremy Dawes | [email protected]

コメント