公開
Somewhere Now — 5,000 地点超のライブカメラを Worker + KV + Cron で生かし続ける
ライブ配信は終わり videoId は変わる。カメラ定義と生存状態を分け、Cron が YouTube Data API で追いかける設計と、サブリクエスト上限・同時発火・waitUntil で失敗を飲み込む罠。
Somewhere Now は、世界 118 の国と地域のライブカメラを地図と地球儀から選んで覗くアプリです。2026-08-27 に 5,720 地点に達し、その後埋め込みが禁止された 9 件を除いて 5,711 地点を収録しています。
このアプリの失敗モードは「死んだリンクだらけの地図」です。ライブ配信は終わり、videoId は変わり、埋め込みを禁止している配信もあります。設計の中心は生存状態の維持にあります。
定義と状態を分ける
カメラの定義(名前・座標・IANA タイムゾーン・配信元・配信タイトル)は src/data/cams.ts に人が書く静的データです。生存状態(解決済み videoId・live/offline/blocked・視聴者数)は Cloudflare KV に置き、/api/cams で配ります。フロントエンドはマスタを持っているので、/api/cams が落ちても地図は出ます。
状態を更新するのは Worker の Cron Trigger です。*/10 * * * * で videos.list(1 unit / 50 件)による生存確認を回し、5 * * * * で消えた配信をチャンネル単位で再探索します。YouTube の API キーは Worker の secret だけにあり、ブラウザには出ません。
サブリクエスト上限で毎回落ちていた
本番で最初に起きたのは Too many subrequests by single Worker invocation でした。5,720 台を 50 件ずつ割ると 115 回の videos.list になりますが、Workers は 1 呼び出しあたりのサブリクエストが 50 で頭打ちです。途中で例外になり、状態の書き込みはスキップされ、finally の台帳書き込みだけが走って 51 unit を毎回捨てていました。KV に残っていた状態は 5,720 台のうち 1,008 台で、82% が未確認のままでした。
2026-08-28 の修正では 1 回の呼び出し数に上限を設け(コード上の定数は MAX_LIST_CALLS_PER_SWEEP = 38)、確認がいちばん古いカメラから順に詰めるようにしました。実行のたびに対象がひとりでに入れ替わるので、どこまで見たかを覚えなくても 3 回・30 分で全件を一巡します。見送ったカメラは queried に入れないので、offline と誤判定しません。
再探索側も同じ罠を踏みました(PR #30)。1 チャンネルで uploads プレイリストを最大 3 ページ辿ると 6 回の呼び出しになり、24 チャンネルで最悪 144 回です。件数ではなく実測した呼び出し回数でブレーキを掛けるようにしました。AGENTS.md に残した教訓は「上限に効くのは unit ではなく呼び出し回数」です。search.list は 100 unit でも 1 回です。
クォータと KV の会計
無料枠は 10,000 units/日ですが、5,720 件を 10 分ごとに全確認すると 16,560 units/日になります。消費量を KV の日次台帳に積み、8,000 units で当日の呼び出しを止めます。例外時も finally で台帳を書くので、キーが不正なまま Cron が回り続けても枠を焼き切りません。
台帳を 1 つの財布にしていたときは、10 分ごとの生存確認が消費の約 70% を食い、毎時の再探索が枯れました(PR #28 で役割別に分割)。さらに Cloudflare から「KV の日次操作が 50%」の通知が来ました。1 回 3 書き込み × 毎時 7 回 = 504 回/日で、無料枠 1,000 回/日の 50.4% です。YouTube のクォータを 45% 減らした最適化が、KV の書き込みを 2.6 倍にしていました。台帳を状態のペイロードに同居させて 2 書き込みにし、33.6% まで下げています(PR #36)。
同時発火と静かな死
*/10 と 0 * の Cron は毎正時に同時に発火します。KV にはアトミック操作がないので後勝ちになり、片方の消費量や状態が消えます。書き手を役割ごとに分け、最終的に再探索を :05 にずらしました。
もう 1 つは ctx.waitUntil の中で起きた例外が誰にも届かない問題です。更新が 3.6 時間止まり、台帳も状態も変わらず、ログも空でした。KV の読み取りが try の外にあったのが原因です。今は入口で必ず [cron <role>] start を 1 行出し、本体を try で包んで「発火しなかった」と「発火して落ちた」を区別できるようにしています。
配信を取り違えない
5,711 台は 2,450 チャンネルにぶら下がり、EarthCam だけで 47 台あります。チャンネルから適当な 1 本を取ると、Times Square のピンに New Jersey の映像が映ります。マスタに配信タイトルを持たせて照合し、括弧の中だけが違う紛らわしいタイトルは厳密に扱います。確信が持てなければ映しません。誤った映像を出すより映さない方がよい、という判断です。
参考
- tktk7l9/somewhere-now — README「この設計で一番効いていること」「使っているクォータ」、AGENTS.md の教訓一覧
- PR #28・#29・#30・#36(台帳分割・Cron の静かな死・再探索の上限・KV 書き込み削減)
- PR #59・PR #60(Effect 4 化と CPU 時間削減)