[OpenNext] Cloudflare Workersで時間経過後に500エラー:Dummy queue is not implemented の原因と対策

発生した問題

@opennextjs/cloudflareを使ってCloudflare Workers上でNext.jsを動かしている環境で、デプロイ直後は正常に表示されるが、時間が経つと動的ページがすべて500エラーになるという問題が発生した。

エラーログには以下のメッセージが記録されていた。

Error in routingHandler FatalError: Dummy queue is not implemented

影響を受けたのは動的ページ全般で、静的ページには問題がなかった。

エラーログの確認方法:Observabilityの有効化

500エラーが返ってくるだけでは原因がわからない。Cloudflare Workersのエラーを確認するために、wrangler.jsoncObservabilityを有効にする。

{
  // ...その他の設定
  "observability": {
    "enabled": true
  }
}

設定を追加して再デプロイすると、Cloudflareダッシュボードの Workers & Pages > 対象Worker > Observability タブからリクエスト単位のエラー情報を確認できるようになる。

補足: observability.enabledtrueにすると、Workers Logsが有効になり、リクエスト単位のログ(エラーメッセージ、リクエスト情報など)がCloudflareダッシュボードのObservabilityタブから確認できるようになる。head_sampling_rateでログを記録するリクエストの割合を制御でき、デフォルトは1(全リクエスト記録)。Workers Logsは無料プランでも1日あたり20万イベントまで利用でき(保持期間3日)、有料プランでは月2,000万イベントまで無料(保持期間7日)となっている。

今回はこのObservabilityタブから以下のようなエラーを確認できた。

{
  // ログレベルとエラーメッセージ本文
  "level": "error",
  "message": "Error in routingHandler FatalError: Dummy queue is not implemented",
 
  // Workerの実行情報
  "$workers": {
    "truncated": false,
    "event": {
      "request": {
        // エラーが発生したリクエストのURL
        "url": "https://example.com/note?_rsc=vusbg",
        "method": "GET",
        "path": "/note",
        "search": {
          "_rsc": "vusbg"  // Next.jsのRSC(React Server Components)用パラメータ
        }
      }
    },
    "outcome": "ok",
    "scriptName": "my-worker",       // Workerのスクリプト名
    "eventType": "fetch",
    "executionModel": "stateless",
    "scriptVersion": {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"  // デプロイバージョンのID
    },
    "requestId": "xxxxxxxxxxxxxxxx"  // リクエストの一意なID
  },
 
  // Cloudflareが付与するメタデータ
  "$metadata": {
    "id": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
    "requestId": "xxxxxxxxxxxxxxxx",
    "trigger": "GET /note",          // エラーのトリガーとなったリクエスト
    "service": "my-worker",
    "level": "error",
    "error": "Error in routingHandler FatalError: Dummy queue is not implemented",
    "message": "Error in routingHandler FatalError: Dummy queue is not implemented",
    "account": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",  // CloudflareアカウントID
    "type": "cf-worker",
    "fingerprint": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",  // 同一エラーをグルーピングするためのハッシュ
    "origin": "fetch",
    "messageTemplate": "Error in routingHandler FatalError: Dummy queue is not implemented"
  }
}

ポイントは以下の通り。

  • messageに原因となるエラーメッセージが記録されている
  • $workers.event.requestでどのURLへのリクエストでエラーが起きたか特定できる
  • _rscパラメータはNext.jsのReact Server Componentsによるクライアントナビゲーション時に付与されるもので、これ自体はエラーの原因ではない
  • $metadata.fingerprintで同じ種類のエラーがグルーピングされるため、同一エラーの発生頻度を把握しやすい

原因:ISRのリバリデーションキューが未設定

Next.jsのレンダリング方式とISR

Next.jsには主に3つのレンダリング方式がある。

方式 説明 サーバー処理
SSG(Static Site Generation) ビルド時にHTMLを生成。以降は静的ファイルとして配信 不要
ISR(Incremental Static Regeneration) ビルド時にHTMLを生成し、一定間隔でバックグラウンド再生成 キャッシュ期限切れ時に必要
SSR(Server-Side Rendering) リクエストのたびにサーバーでHTMLを生成 毎回必要

ISRとSSRはどちらもサーバー側の処理が必要なため、Cloudflare Workers上では動的ページとして扱われ、リクエスト時にWorkerのルーティング処理を通る。

一方、SSGで生成されたページは完全な静的ファイルとしてCloudflare Workersの静的アセットから配信されるため、Workerのルーティング処理を経由しない。

ISRはrevalidateオプションでキャッシュの有効期限(秒数)を指定する。キャッシュが有効な間はキャッシュされたレスポンスを即座に返し、期限切れ後のリクエスト時には古いキャッシュをそのまま返しつつ、バックグラウンドでページを再生成する(stale-while-revalidate)。再生成が完了すると、次回以降は新しいページが返される。

このサイトでの各ページの配信方式

本サイトでは以下のようにレンダリング方式が分かれている。

ページ 方式 revalidate
/note(記事一覧) ISR 3600秒(1時間)
/schedule(スケジュール) ISR 3600秒(1時間)
/changelog(更新履歴) ISR 3600秒(1時間)
/(トップ) SSG -
/contact(お問い合わせ) SSG -
/profile(プロフィール) SSG -
/onidazo(おにだぞ) SSG -

今回エラーが発生していたのは /note/schedule/changelog のISRページであり、SSGの /(トップ)、/contact/profile などでは問題が起きていなかった。ISRページはキャッシュの有効期限が1時間に設定されているため、デプロイから約1時間後にエラーが発生し始めるという挙動だった。

キューが未設定だった

ISRのバックグラウンド再生成はキューを通じて処理される。

しかし、open-next.config.tsでキューの設定を省略すると、デフォルトでdummyキューが使われる。このdummyキューはsend()を呼んだ瞬間にFatalErrorをスローする実装になっている。

// @opennextjs/aws内部のdummyキュー実装
const dummyQueue = {
  name: "dummy",
  send: async () => {
    throw new FatalError("Dummy queue is not implemented");
  },
};

つまり、以下の流れでエラーが発生していた。

  1. デプロイ直後はキャッシュが有効なため、ページは正常に表示される
  2. キャッシュの有効期限が切れる
  3. 次のリクエスト時にISRがリバリデーション(再生成)を試みる
  4. リバリデーションをキューに送ろうとする
  5. dummyキューがFatalErrorをスローする
  6. 500 Internal Server Error

「デプロイ直後は動くが、時間が経つと壊れる」という挙動はこのメカニズムで説明がつく。

静的ページ(SSG)はWorkerのルーティング処理を通らないため、ISRのキューも呼ばれずエラーが発生しない。 影響を受けるのはISRを使用する動的ページのみである。

解決方法:memory-queueを設定する

Cloudflare Workers向けには、@opennextjs/cloudflareが提供するmemory-queueを使用する。

1. open-next.config.tsにキュー設定を追加

import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
import memoryQueue from "@opennextjs/cloudflare/overrides/queue/memory-queue";
 
export default defineCloudflareConfig({
  incrementalCache: staticAssetsIncrementalCache,
  queue: memoryQueue,
  enableCacheInterception: true,
});

2. wrangler.jsoncにサービスバインディングを追加

memory-queueは自分自身のWorkerにHEADリクエストを送ることでリバリデーションを実行する。そのため、自身を参照するサービスバインディングが必要になる。

{
  "main": ".open-next/worker.js",
  "name": "my-project",
  "compatibility_date": "2026-02-13",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  },
  // 追加:自身のWorkerを参照するサービスバインディング
  "services": [
    {
      "binding": "WORKER_SELF_REFERENCE",
      "service": "my-project"  // wrangler.jsoncの"name"と一致させる
    }
  ]
}

serviceの値はwrangler.jsoncnameと一致させること。

3. 再デプロイ

変更後に再デプロイすれば、ISRのリバリデーションが正常に動作するようになる。

memory-queueの仕組み

memory-queueの動作は以下の通り。

  1. ISRが再生成を要求すると、memory-queuesend()が呼ばれる
  2. Worker内のメモリで重複排除(同じページへの複数リバリデーションを防ぐ)を行う
  3. WORKER_SELF_REFERENCEサービスバインディング経由で自分自身にHEADリクエストを送信する
  4. HEADリクエストのヘッダーにx-prerender-revalidateを含めることで、Next.jsがページを再生成する

なお、Cloudflare Workers向けにはもう一つdurable-queue(Durable Objectsベース)も用意されている。こちらは永続的な状態管理とより確実な重複排除・リトライ機能を提供するが、Durable Objectsの利用料金が発生するため、小〜中規模のサイトではmemory-queueで十分である。

まとめ

  • @opennextjs/cloudflareでISR対応のNext.jsサイトを動かす場合、キューの設定は必須
  • キューを設定しないとデフォルトのdummyキューが使われ、キャッシュ期限切れ後に500エラーになる
  • memory-queue + WORKER_SELF_REFERENCEサービスバインディングで解決できる
  • 「デプロイ直後は動くが時間が経つと壊れる」場合はISRキューの設定を確認する