はじめに
ある日の夕方、Cloudflareからこんな通知が届きました。
アカウントは Workers および/または Pages Functions の1日のリクエスト制限の 91% に達しました
時刻はJST 16:46。リクエスト数のリセットはUTC 0:00(JST 9:00)なので、その日はまだ7時間46分しか経っていません。このペースだと、日付が変わる前に上限へ達します。
上限に達するとどうなるか。Cloudflareの説明はこうです。
何も対処しない場合、制限がリセットされるまでお客様の Worker(s) が失敗する可能性があります。
運用しているのは先日公開したこちらの記事:AIとともに、ふりかえりの総合サイト hurikaeri.jp を作りました(手法カタログ144種・全無料) で作ったサイトになります。
静的サイトなので、ページの表示自体はWorkerを経由しません。それでも10万リクエストに迫っている。明らかに何かがおかしい。
結論から言うと、原因は公開していたMCPサーバが、接続してきた全クライアントを無限リトライループに陥らせていたことでした。しかも仕様違反はこちら側にありました。
この記事では、起こったこと・原因・対策を順に書きます。同じようにMCPサーバを公開しようとしている方の役に立てば幸いです。
この記事はMCPの仕様(Streamable HTTP)に関する話を含みます。仕様は更新される可能性があるため、実装時は必ず公式の仕様書を確認してください。
対象読者
- MCPサーバを自作して公開している、あるいはこれから公開しようとしている人
- Cloudflare Workersの無料枠で個人開発をしている人
- 「なぜかリクエスト数が異常に多い」という状況に心当たりのある人
前提:どんな構成だったか
サイトの構成はシンプルです。
- 静的サイト(Astroでビルド)をCloudflare Workersで配信
-
/api/*と/mcpだけをWorkerが処理する - それ以外は静的アセットとして返す(Workerを起動しないのでリクエスト数にカウントされない)
この設定はwrangler.tomlに書いてあります。
[assets]
directory = "./dist"
binding = "ASSETS"
run_worker_first = ["/api/*", "/mcp"]
run_worker_firstに書いたパスだけWorkerが先に受け取ります。つまり10万リクエストを消費しているのは/api/*と/mcpだけということになります。
ちなみに/mcpがあるのは、2026年7月22日(この記事を書いている1.5日前)にふりかえり手法カタログをMCPサーバとして公開したからです。Claude CodeなどのMCPクライアントから、147種類のふりかえり手法を検索・取得できるようにしていました。
起こったこと
数字を見る
まずCloudflareのメトリクス画面を見ました。15分間の数字がこうです。
| 項目 | 値 |
|---|---|
| 呼び出し | 11k |
| サブリクエスト | 73 |
| エラー | 0 |
| CPU時間 | 0.3ms |
| ウォールタイム | 0.54ms |
15分で11,000リクエスト。1時間で44,000、1日換算で約100万リクエストです。無料枠の10倍。
ここで2つ、引っかかる点がありました。
1つめ。サブリクエストが73件しかない。
サブリクエストは、Workerの中から外部に出ていくfetchの数です。11,000回呼び出されて外部アクセスが73件ということは、大半のリクエストは外部を一切呼んでいない。
2つめ。CPU時間0.3ms、ウォールタイム0.54ms。
極端に短い。KVストレージを読んで返すような処理ですらこの数字にはなりません。ほぼ何もせずに即座に返しているリクエストです。
この2つを合わせると、犯人像が見えてきます。外部fetchもストレージアクセスもせず、即座にレスポンスを返す経路が、大量に叩かれている。
パス別の内訳が見られない
犯人像は分かったものの、どのパスなのかが分かりません。
Cloudflareには「Observability」というログ収集機能があるのですが、これがオフになっていました。
有効にしてもログが取れるのは有効化した後からで、すでに消費した91%の内訳は永久に分かりません。
ここが今回いちばんの反省点です。Observabilityは事前に有効化しておくべきでした。 事後では手遅れです。
とはいえ1日100万リクエストのペースなら、有効化すれば数分で十分なサンプルが集まります。すぐ有効化しました。
Cloudflareのダッシュボードから設定することもできますが、このサイトは「設定はコードで管理する」という方針にしているので、wrangler.tomlに書いてデプロイしました。
[observability]
enabled = true
[observability.logs]
enabled = true
invocation_logs = true
あわせて、Worker側にログを1行だけ仕込みます。
async fetch(request: Request, env: Env, ctx): Promise<Response> {
const url = new URL(request.url);
// 調査用ログ: どのパスが叩かれているか特定する
console.log('[req]', request.method, url.pathname,
(request.headers.get('user-agent') ?? '').slice(0, 80));
デプロイして数分待ち、ログを開きました。
犯人が見つかる
2026-07-24 16:53:13.778 JST [req] GET /mcp claude-code/2.1.218 (cli)
2026-07-24 16:53:13.572 JST [req] GET /mcp claude-code/2.1.218 (cli)
2026-07-24 16:53:13.110 JST [req] GET /mcp claude-code/2.1.218 (cli)
2026-07-24 16:53:12.095 JST [req] GET /mcp claude-code/2.1.218 (cli)
2026-07-24 16:53:11.729 JST [req] GET /mcp claude-code/2.1.218 (cli)
2026-07-24 16:53:11.710 JST [req] GET /mcp claude-code/2.1.218 (cli)
2026-07-24 16:53:11.080 JST [req] GET /mcp claude-code/2.1.218 (cli)
2026-07-24 16:53:10.695 JST [req] GET /mcp claude-code/2.1.218 (cli)
2026-07-24 16:53:10.531 JST [req] GET /mcp claude-code/2.1.218 (cli)
GET /mcpが0.2秒間隔で延々と続いています。3秒間で9リクエスト。
心当たりがありました。自分もClaude CodeからこのMCPサーバに繋いでいます。試しに接続を切ってみました。
……ログは出続けました。
しかも今度は別のクライアントが混ざっています。
[req] GET /mcp claude-code/2.1.218 (cli)
[req] GET /mcp claude-code/2.1.208 (claude-vscode, agent-sdk/0.3.208)
バージョンも環境も違う。つまり、このMCPサーバに接続している不特定多数のクライアント全員が、同じループに入っているということです。
自分が接続を切っても止まらない。これは完全にサーバ側の問題です。
問題:なぜループしていたのか
MCPのGETとPOST
MCPのStreamable HTTPという通信方式では、/mcpエンドポイントに対してGETとPOSTの両方が使われます。役割が違います。
- POST … クライアントからの質問。「手法を検索して」「詳細をちょうだい」。普通の一往復のやり取り
- GET … サーバからクライアントへの一方的な通知用。SSE(Server-Sent Events)でストリームを開き、接続を維持したまま待ち受ける
SSEは、接続を開いたままサーバ側から好きなタイミングでデータを送り続ける仕組みです。株価がリアルタイム更新される画面のような用途で使われます。
MCPの場合、「ツールの一覧が変わったよ」といった通知をサーバから送りたいときのための回線です。
実装がどうなっていたか
ふりかえり手法カタログは、聞かれたら答えるだけのサーバです。サーバから勝手に通知を送る必要がありません。なのでSSEは実装していませんでした。
ではGETが来たらどうしていたか。こうです。
export async function handleMcpRequest(request: Request, load: DataLoader) {
if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: CORS });
if (request.method === 'GET') {
return json(
{
name: 'hurikaeri-mcp',
description: 'ふりかえり手法カタログのMCPサーバ。POSTでMCP(Streamable HTTP)を話します。',
endpoint: 'https://.../mcp',
site: 'https://hurikaeri.jp',
tools: TOOLS.map((t) => t.name),
},
200, // ← これが問題
);
}
// POSTの処理が続く
サーバの情報をJSONで返していました。 ブラウザでURLを開いた人に「ここはMCPサーバですよ」と伝えるつもりの、親切心からの実装です。
これが完全に裏目に出ました。
クライアントから見た挙動
MCPクライアントの立場になると、こう見えます。
- 「通知を受け取る回線をつなぎます」とGETを送る
- サーバが何かJSONを返して、接続を即座に閉じる
- クライアント「あれ、ストリームが切断された。つなぎ直そう」
- 1に戻る
電話をかけたら即ガチャ切りされて、かけ直し続けている状態です。これが0.2秒間隔で、接続中の全クライアント分。
しかもタチが悪いことに、エラーは1件も出ていません。200 OKを返しているので、Cloudflareのメトリクス上は「正常なリクエスト」としてカウントされます。エラー率0%のまま無料枠だけが溶けていきます。
仕様を確認する
MCPの仕様書(Streamable HTTP transport)にはこう書かれています。
The server MUST either return
Content-Type: text/event-streamin response to this HTTP GET, or else return HTTP 405 Method Not Allowed, indicating that the server does not offer an SSE stream at this endpoint.
意訳すると「サーバはGETに対して、SSEストリームを返すか、405 Method Not Allowedを返すか、そのどちらかでなければならない」。SSEを提供しないなら405を返せ、と明確に定められています。
200でJSONを返すのは仕様違反でした。 クライアントは仕様どおりの前提で動いているので、想定外のレスポンスを受け取って混乱していた、というのが実態です。
対策
GETに405を返す
修正はシンプルです。
export async function handleMcpRequest(request: Request, load: DataLoader) {
if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: CORS });
// GETはStreamable HTTP仕様ではSSEストリームを開く要求。このサーバはSSE非対応なので
// 405を返す(仕様上の正しい応答)。以前はサーバ情報のJSONを200で返していたが、
// クライアントは「ストリームが即座に閉じられた」と解釈して再接続を繰り返し、
// 1秒間に数回のリトライループに陥っていた
if (request.method === 'GET') {
return new Response(null, {
status: 405,
headers: { allow: 'POST, OPTIONS', ...CORS },
});
}
// POSTの処理が続く
405は「SSEはやっていません」という明示です。クライアントはこれを受け取れば納得してリトライをやめます。
ブラウザからのアクセスはどうするか
ここでひとつ問題があります。ブラウザで/mcpを開いた人にも405を返してしまうと不親切です。
このサイトでは、MCPサーバの紹介ページを/mcp/に置いていました。なのでリクエストヘッダのacceptを見て振り分けます。
if (url.pathname === '/mcp' || url.pathname === '/mcp/') {
// ブラウザ(text/html)は紹介ページへ
if (request.method === 'GET' && (request.headers.get('accept') ?? '').includes('text/html')) {
if (url.pathname === '/mcp') return Response.redirect(new URL('/mcp/', url.origin).toString(), 301);
return env.ASSETS.fetch(request);
}
// MCPクライアントからのリクエストはこちら
return handleMcpRequest(request, loadData);
}
ブラウザはaccept: text/htmlを送ってくるので、これで振り分けられます。MCPクライアントはHTMLを要求しないので、405の処理に流れます。
結果
デプロイして数分後、ログを見ました。
17:18:10 POST /mcp
17:17:29 POST /mcp claude-code/2.1.218 (cli)
17:16:05 GET /api/likes Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 ...)
17:10:13 GET /api/events Mozilla/5.0 (Windows NT 10.0; Win64; x64)
17:10:06 GET /api/comments?slug=ywt
17:07:12 GET /api/events
17:04:47 GET /api/comments?slug=yakiniku-retrospective
17:04:34 GET /api/comments?slug=takibi
17:04:12 GET /api/events
17:03:57 GET /api/comments?slug=555
15分間で十数件になりました。修正前は同じ15分で11,000件でしたから、1000分の1以下です。
内容も健全です。POST /mcpは実際のMCP利用(ループのGETではなく、ちゃんと質問が来ている)。GET /api/comments?slug=takibiはサイトの実践メモ機能、/api/likesはiPhoneからの「♥すき」の表示。すべて人間の操作に紐づいたリクエストで、間隔も自然です。
これなら無料枠に対して十分な余裕があります。課金は不要でした。
学んだこと
1. 仕様で意味が決まっているメソッドに、独自の応答を返さない
GET /mcpはMCPの仕様上「SSEストリームを開く要求」と意味が決まっています。そこに「サーバの情報を返そう」という親切心を差し込んだのが今回の原因でした。
親切心そのものは悪くないのですが、やるならブラウザからのアクセスに限定すべきでした。実際、最終的な実装はそうなっています。
ちなみにその親切心を出したのは Claude Fable 5、君だよ
2. 公開したサーバは、自分が接続を切っても止まらない
これは当たり前のようでいて、実際に体験するとインパクトがあります。
自分のClaude Codeを切断してもログが出続けたとき、はじめて「これは自分だけの問題ではない」と理解しました。MCPサーバを公開するというのは、不特定多数のクライアントが自分の実装のバグを一斉に踏みにくるということです。
3. Observabilityは事前に有効化しておく
今回いちばんの反省点です。
Observabilityがオフだったせいで、消費済みの91%の内訳は永久に分かりませんでした。有効化して初めて原因が見えたわけですが、もし気づくのがもっと遅ければ、原因不明のままサービスが停止していました。
Cloudflare Workersの場合、無料プランでも1日20万イベントまでログが取れます。最初から有効にしておいて損はありません。
4. エラー率0%でも異常はある
今回、Cloudflareのメトリクス上のエラーは終始0件でした。200 OKを返し続けていたからです。
「エラーが出ていないから正常」とは限りません。今回の場合、異常のシグナルは以下に出ていました。
- リクエスト数の異常な多さ
- サブリクエストの少なさ(外部を呼んでいない=何もしていない)
- CPU時間の短さ(処理していない)
この3つの組み合わせが「即座に返しているだけの経路が大量に叩かれている」を示していました。エラー率以外の指標も見る癖をつけたいところです。
おわりに
MCPサーバを公開する側の記事はまだ多くありません。特に「公開したあとに何が起きるか」の実例は少ないと感じています。
調べた範囲では、英語圏のGitHub Issueに「405を返すべき」という議論はいくつかありました。ただそれらは主に「405を返すサーバにクライアントが繋げない」というクライアント側の不具合報告で、200を返した結果どうなるかを実測した例は見当たりませんでした。
もし同じようにMCPサーバを公開しようとしている方がいたら、GETの扱いだけは仕様書を確認してから実装することをおすすめします。SSEを実装しないなら405、これだけ覚えておけば同じ轍は踏まずに済みます。
そして繰り返しになりますが、Observabilityは先に有効化しておきましょう。
ふりかえり手法カタログのMCPサーバは、修正後も引き続き公開しています。147種類のふりかえり手法とTIPSを検索・取得できます。よかったら使ってみてください。
- サイトの紹介: AIとともに、ふりかえりの総合サイト hurikaeri.jp を作りました(手法カタログ144種・全無料)
- MCPサーバの紹介: https://hurikaeri.jp/mcp/

