きっかけ
個人開発しているfusionpapersというサイトに、ChatGPTのCustom GPT Actions経由でアクセスできる機能を作りました。核融合の論文を検索できるツールで、試しにGPTへ「トカマク型核融合炉の閉じ込め改善に関する最新研究を教えて」と聞いてみたところ、専用ツールが応答せず、GPTが勝手にWeb検索へフォールバックしました。
ログを見ると429 Too Many Requestsが返っています。作ったばかりの機能がいきなり詰まっているので、まずは「実装をミスったかな」と思って調べ始めました。
クォータが本当に枯渇していた
fusionpapersのAPIはAPI GatewayのUsage Planで月間リクエスト数を制限しています。当時のCDK設定はこうでした。
// backend/cdk/lib/fusionpapers-v2-stack.ts
const usagePlan = api.addUsagePlan("UsagePlan", {
name: `fusionpapers-v2-usage-${stage}`,
throttle: { rateLimit: 10, burstLimit: 20 },
quota: { limit: 5000, period: apigateway.Period.MONTH },
});
aws apigateway get-usageで直近の消費量を見てみると、想像以上のことになっていました。
day 24: used=162 remaining=4838
day 25: used=368 remaining=4470
day 26: used=4470 remaining=0 ← ここでクォータ完全枯渇
1日で4,000件以上溶けています。月5,000件の枠を1日で食い尽くした計算です。GPT Actionsをちょっと試しただけでこんなに減るはずがないので、これは実装ミスというより「何か」が大量にリクエストを送っている、という方向で調査を進めることにしました。
Lambdaのログを見ると、GPTが使わないエンドポイントまで叩かれている
CloudWatch Logs InsightsでバックエンドLambdaのアクセスログを見てみます。
/papers/{key}への直近48時間のリクエストは4,159件で、うち92.8%(3,858件)が別々の論文ページへの初回アクセスでした。同じページへの繰り返しではなく、ほぼ全部ユニークです。
同時間帯には/journals/*/reports、/devices/*/analytics、/search/analytics?q=ITERにもアクセスがありました。サイトのほぼ全ページ種別が対象になっています。
ここで「あれ」と思いました。GPTのツール(search_papersなど)はこれらのエンドポイントを持っていません。つまりGPT Actions経由の通信ではなく、サイトを人間のようにブラウジングしている何かがいる、ということになります。
さらに数字を追うと、48時間ずっと同じ勢いだったわけではなく、途中にピークがありました。そのピークの約3時間だけでAPI Gatewayに約9,035件が429で弾かれていて、Lambdaに到達した約4,958件と合わせると、同時間帯だけで約14,000件相当のリクエスト量です。レスポンス時間もLambdaのハードタイムアウト(30秒)に張り付いていて、バックエンドが実際に悲鳴を上げていたのも確認できました。
もしかして外部からの攻撃をうけたのかも、っと思いました。
CloudFrontの実アクセスログで、本物のIPが見えた
Amplify/CloudFrontのコンソールから取得できるアクセスログのCSVエクスポート(4,370行)を見てみます。ここには実際のクライアントIP・User-Agent・Refererが記録されていて、ようやく手がかりが出ます。
- 単一のIPから4,003件(全体の91.6%)が発生していた
- そのUser-Agent内訳
-
Chrome/150.0.0.0 (Macintosh...)が4,001件 —/、/about、/devices、/journals、/papers/{key}など、サイト全体を巡回している -
curl/8.7.1が1件 -
Claude-User (claude-code/2.1.220; +https://support.anthropic.com/)が1件
-
後半の2件は、この調査中に自分がClaude Codeで動作確認のために叩いたリクエストと一致します。つまりこのIPは、自分の作業環境そのものの送信元IPでした。
ローカルを見たら、Playwright MCPが18時間動きっぱなしだった
自分のマシンのプロセスを確認すると、こんなものが動いていました。
<user> 1820 .../npx @playwright/mcp@latest --user-data-dir /path/to/playwright-data
<user> 1825 npm exec @playwright/mcp@latest --user-data-dir /path/to/playwright-data
<user> 1895 node .../playwright-mcp --user-data-dir /path/to/playwright-data
Claude.app(デスクトップ版)経由で起動された、実ブラウザ(Chromium)を操作するMCPサーバーです。起動時刻を見ると、発見時点で経過時間は約18時間。CPU使用率は0.0%でアイドル状態でした。実ブラウザなのでUser-Agentは普通のChromeと見分けがつかず、さっきCloudFrontログで見つけたUAと一致するのも当然でした。
自分の記憶を辿ってみると、確かに以前の開発セッションで、Claude CodeがいきなりPlaywright関連のコマンド実行許可を求めてきたことがありました。「これは何をするコマンドで、どれくらいの範囲・時間動くのか」を説明もなく、ただ許可を求めるダイアログが出ただけです。当時は動作確認の一環だろうと思って、内容をよく確認せずに許可しました。
そのセッションを終えたつもりが、MCPサーバー自体はバックグラウンドで生き続けていて、断続的にサイトへアクセスし続けていたようです。ログを見ると2026-07-25 00:44(UTC)から少なくとも48時間以上継続していました。ベースの通信は数分〜1時間半おきに約5件ずつという、いかにも「ブラウザが律儀にページ遷移してますよ」というパターンで、そこに前述のピーク(3時間で約14,000件相当)が乗っていた形です。発見時点のプロセスの経過時間は約18時間だったので、過去に同じような自動巡回セッションを何度か立ち上げては、終了し忘れていた可能性が高いです。
結論
429エラーの原因は外部の攻撃でも、GPT Actionsの実装不備でもありませんでした。開発中に立ち上げたPlaywright MCPが本番サイトを自動巡回し続け、月間APIクォータ(5,000件/月)を自分で溶かしていたというのが真相です。
もう少し正確に言うと、原因はPlaywrightそのものより、その手前の自分にあります。Claude Codeが出したPlaywright関連のコマンド実行許可ダイアログを、中身も確認せずに通したのがすべての始まりでした。
犯人探しのつもりで、Usage Plan→Lambdaログ→CloudFrontログ→ローカルプロセスと辿っていったら、最後に鏡を見せられた気分です。ちなみにPlaywright自体はkillして止めました。
教訓
以下を学びました。
- Playwright MCPは裏で生き続ける。ウィンドウを閉じたつもりでも、プロセスとしては動き続けていることがある
- ブラウザ自動操作での動作確認は、本番(
fusionpapers.com)ではなくローカル環境(next dev/uvicorn --reload)を優先する - そもそも本番のUsage Planが月5,000件は小さすぎた。実際、この記事を書いている間に10,000件へ引き上げた。開発中のブラウザテストや今後のGPT利用のような、人間のブラウジング以外の用途も想定して余裕を持たせておくべきだった
今回は個人開発だったので笑い話で済みましたが、これが小規模でも本番運用しているプロダクトだったらと考えると、ちょっと笑えません。本番URL×Playwrightの組み合わせには、もっと慎重になるべきだと学びました。