☁️ Cloudflare 10万リクエスト事件簿
2026-04-04 更新
事件の概要
Cloudflare Workers & Pages の無料枠は 10万リクエスト/日。 これが April 1〜3 の3日間で 161.71k リクエスト を消費してしまった。 1日あたり約 49,306 リクエスト(上限の約半分)を常時消費していた計算になる。
無料枠の状況
- 無料枠上限: 100,000 リクエスト/日
- April 1〜3 合計消費: 161,710 リクエスト(3日間)
- 1日平均: 約 53,900 リクエスト
- ピーク日: 49,306 リクエスト(上限の約49%)
調査の結果、ラズパイで動いている bot.py が犯人と判明した。
原因: 3秒ポーリング
bot.py は satomatashikiaichat.pages.dev/api/bot/read に 3秒ごと にリクエストを送り続けていた。
監視対象チャンネルは 2つ(general, newspaper)。
リクエスト数の計算
| 単位 | 計算式 | リクエスト数 |
|---|---|---|
| 1分間 | 2チャンネル × 20回(60秒÷3秒) | 40 req/min |
| 1時間 | 40 × 60分 | 2,400 req/h |
| 1日 | 2,400 × 24時間 | 57,600 req/day |
理論値 57,600 が実測値 49,306 とほぼ一致。ラズパイが落ちていた時間や処理遅延を考慮すると完全に符合する。
なぜポーリングになったか
satomatashikiaichat の API は REST のみ(/read と /post)で設計されていた。
WebSocket や Webhook が用意されていなかったため、新着メッセージを検知するには「定期的に確認するしかない」構造だった。
設計上の問題: プッシュ通知の仕組みがなく、クライアント側が能動的にポーリングするしかない REST-only 設計が根本原因。
DiscordとRESTの違い
Discord bot は同じ「チャットの新着監視」をしているのにリクエストをほぼ消費しない。 その理由は通信方式の根本的な違いにある。
| 項目 | satomatashiki-bot | Discord bot |
|---|---|---|
| 通信方式 | RESTポーリング(3秒ごと) | WebSocket(常時接続) |
| リクエスト発生 | 常に発生(メッセージがなくても) | メッセージが来たときだけ |
| 1日のリクエスト数 | ~57,600回 | ほぼ0 |
| サーバー負荷 | 高(常時Worker起動) | 低(接続維持のみ) |
| リアルタイム性 | 最大3秒遅延 | 即時 |
根本解決策(検討)
ポーリングをやめるための根本的な設計変更案を4つ検討した。
ロングポーリング
サーバーが新メッセージまで応答を保留(最大30秒)。クライアントは応答が来てから次のリクエストを送る。
SSE(Server-Sent Events)
サーバーからクライアントへの一方向プッシュ配信。接続を1本維持するだけでメッセージを受け取れる。
WebSocket
Discord 同様の双方向常時接続。最もリアルタイム性が高いが、Cloudflare での実装には Durable Objects が必要(有料プラン)。
Cloudflare Tunnel + Webhook
ラズパイ側に HTTP エンドポイントを立て、Cloudflare Tunnel で公開。新着メッセージ時にサーバーがラズパイへ Webhook で通知する。
短期的な最善策: ロングポーリングは既存 REST API の変更だけで実装できるため、移行コストが最小。 中長期的には SSE または Webhook が理想。
取った対処
根本解決には時間がかかるため、まず即時停止で出血を止めた。
bot.py の即時停止
# サービスを即時停止
sudo systemctl stop satomatashiki-bot.service
# 再起動時の自動起動も無効化
sudo systemctl disable satomatashiki-bot.service継続しているサービス
配信系スクリプト(news_delivery.py、schedule_notify.py)は cron で1日数回だけ実行するため、リクエスト数への影響は軽微。そのまま継続。
| スクリプト | 実行方式 | 対処 |
|---|---|---|
| bot.py | systemd常駐(3秒ポーリング) | 停止・無効化 |
| news_delivery.py | cron(1日数回) | 継続 |
| schedule_notify.py | cron(1日数回) | 継続 |
教訓
Cloudflare 無料枠の現実
10万リクエスト/日は一見多く見えるが、3秒ポーリング×2チャンネルだけで57,600を消費する。定常的なポーリング設計は無料枠との相性が極めて悪い。
ポーリングは「見えないコスト」を生む
ポーリングはシンプルに見えるが、実際には24時間止まらず静かにリクエストを消費し続ける。ダッシュボードを定期的に確認しないと気づかない。
SSRページはWorkerリクエストを消費する
SSR が有効なページはページビュー = Worker リクエストになる。コンテンツが静的なページは prerender = true を設定してWorker呼び出しを回避することを検討すること。
Discord/TelegramはWebSocketで同じ問題が起きない
既存のチャットプラットフォームのbotがCloudflareのリクエスト制限を踏むことはほぼない。これらは最初からWebSocketで設計されているため、メッセージがない間はリクエストがゼロだからだ。
REST-only APIは自作botと相性が悪い
APIを設計する時点で「botがポーリングする」ユースケースを想定していれば、ロングポーリングやSSEのエンドポイントを最初から用意できた。後付けで変更するコストは高い。