さとまたwiki

☁️ 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.pysatomatashikiaichat.pages.dev/api/bot/read3秒ごと にリクエストを送り続けていた。 監視対象チャンネルは 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-botDiscord bot
通信方式RESTポーリング(3秒ごと)WebSocket(常時接続)
リクエスト発生常に発生(メッセージがなくても)メッセージが来たときだけ
1日のリクエスト数~57,600回ほぼ0
サーバー負荷高(常時Worker起動)低(接続維持のみ)
リアルタイム性最大3秒遅延即時

根本解決策(検討)

ポーリングをやめるための根本的な設計変更案を4つ検討した。

1

ロングポーリング

サーバーが新メッセージまで応答を保留(最大30秒)。クライアントは応答が来てから次のリクエストを送る。

削減効果: 1/30(約1,920 req/day) 実装難度: 低
2

SSE(Server-Sent Events)

サーバーからクライアントへの一方向プッシュ配信。接続を1本維持するだけでメッセージを受け取れる。

削減効果: ほぼ0(接続維持のみ) 実装難度: 中(実装中)
3

WebSocket

Discord 同様の双方向常時接続。最もリアルタイム性が高いが、Cloudflare での実装には Durable Objects が必要(有料プラン)。

削減効果: ほぼ0 実装難度: 高(Durable Objects = 有料)
4

Cloudflare Tunnel + Webhook

ラズパイ側に HTTP エンドポイントを立て、Cloudflare Tunnel で公開。新着メッセージ時にサーバーがラズパイへ Webhook で通知する。

削減効果: ほぼ0 実装難度: 中(インフラ変更が必要)

短期的な最善策: ロングポーリングは既存 REST API の変更だけで実装できるため、移行コストが最小。 中長期的には SSE または Webhook が理想。

取った対処

根本解決には時間がかかるため、まず即時停止で出血を止めた。

bot.py の即時停止

# サービスを即時停止
sudo systemctl stop satomatashiki-bot.service

# 再起動時の自動起動も無効化
sudo systemctl disable satomatashiki-bot.service

継続しているサービス

配信系スクリプト(news_delivery.pyschedule_notify.py)は cron で1日数回だけ実行するため、リクエスト数への影響は軽微。そのまま継続。

スクリプト実行方式対処
bot.pysystemd常駐(3秒ポーリング)停止・無効化
news_delivery.pycron(1日数回)継続
schedule_notify.pycron(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のエンドポイントを最初から用意できた。後付けで変更するコストは高い。