ブログ

YouTube APIの1日1万ユニットでやりくりする

要約

  • 新しい動画はYouTubeチャンネルのRSSで取得します。クォータはかかりません。YouTube APIは、RSSが教えてくれないこと(動画の長さ、ライブの状態、チャンネルアイコン、コメント)を埋めるためと、RSSを取得できなかったときに代替取得するために使います。
  • APIで取得したものはチャンネルごとにサーバーにキャッシュし、すべてのユーザーで共有します。そのため、フィードを更新するコストはユーザー数ではなく、異なるチャンネルの数と新しい動画の数に比例します。
  • 直近7日間に実際に使った量は701ユニット、1日約100ユニット(上限の1%)でした。まだ正式リリース前なのでユーザーは私一人、チャンネルは40個です。そして、いちばん多く使っていた場所は予想外でした。

2026-10-30 修正:公開後に変わったことがあるので書いておきます。1人が追加できるチャンネルは30です(記事の「40チャンネル」はサーバーのキャッシュにあったチャンネル数です)。未取得のチャンネルも、今はRSSが失敗すると、存在するチャンネルかを先に確認してから代替取得します(11月3日の記事で詳しく)。サーバーのキャッシュにあるYouTubeの情報は、ポリシーの30日より1日早い29日以内に削除します。

2026-11 修正:1人が追加できるチャンネルを100チャンネルに増やしました。アプリは今も30チャンネルずつサーバーに問い合わせます。11月7日にCloudflareの有料プラン(月5ドル)に移行し、1日の予算も増やしました。この記事の「無料プラン」と予算の数字は10月時点のものです。

Plainfeedは、選んだYouTubeチャンネルの新しい動画だけを、おすすめやショートなしで投稿された順に表示するウェブサイトです。サーバーはCloudflareの無料プラン1つで動かしています。

こういうものを作ると最初にぶつかる壁が、YouTube Data APIのクォータです。1プロジェクトにつき1日1万ユニットで、お金を払って増やす方法はありません。増やすには監査を受ける必要があります。この記事は、その1万ユニットの中で登録チャンネルのフィードを回すために下した決定と、その決定が実際にどれだけ持ちこたえているかを数字でまとめた記録です。

先に用語を3つだけ。

クォータはこうなっています

まず守るべき条件を整理しました。2026年10月に公式ドキュメントを読み直して確かめた内容です。

項目 内容
基本のクォータ プロジェクトごとに1日10,000ユニット(ほとんどのメソッドで共有)
search.list、videos.insert 2026年6月から別枠:それぞれ1日100回まで、1回1ユニット
videos.batchGetStats 2026年6月に新設。別枠で1日10,000ユニット、1回1ユニット
一覧の読み取り(videos.listなど) ほとんど1回1ユニット、次のページを取得するとさらに1ユニット
書き込み(チャンネル登録の追加など) ほとんど1回50ユニット
不正なリクエスト 失敗しても最低1ユニット
リセット 太平洋時間の午前0時(日本時間の16時または17時)
増やす方法 申請書を出して監査を受ける必要がある。有料で増やす方法はない

ポリシーも2つかかります。APIで取得したデータは30日以内に取得し直すか削除しなければならず(開発者ポリシー III.E.4)、1つのAPIクライアントにはAPIプロジェクトをちょうど1つだけ作らなければなりません(III.D.1.c)。プロジェクトを複数作ってクォータを分け合う道は、最初からないわけです。

Plainfeed側の条件はこうです。1人が追加できるチャンネルは最初は30チャンネルで(アプリはサーバーに30チャンネルずつ問い合わせます)、新しい動画はふつう投稿から30分以内には表示されなければなりません。

素直にAPIで作ると何ユニットか

まず思いつく方法で計算するとこうなります。

方法 かかる量 限界
チャンネルごとにsearch.listで最新の動画を探す チャンネル1つの確認に1回 1日100回がプロジェクト全体の上限なので、30チャンネルを追加したユーザーが3人いれば1日1回ずつしか確認できない
チャンネルごとのアップロード一覧(playlistItems.list)を30分ごとに チャンネル1つで1日48ユニット 1万ユニットで1日中見られるチャンネルは約208チャンネル
チャンネルのRSS + 新しい動画だけvideos.list(Plainfeed) 新しい動画50本で1ユニット 後述

実はPlainfeedの初日のバージョンは、1つ目の方法を使っていました。当時はYouTubeのカテゴリ別に最新の動画を集めて表示するサイトで、カテゴリごとに3〜8時間おきにsearch.listを呼んでいました。そのとき書いた設計メモには「1回100ユニット」とあります。1日に数回回すだけでクォータの大部分が消える作りだったので、その日のうちにチャンネルのRSSに切り替えました。コンソールの直近30日の記録に残っているsearch.list 31回がその名残です。

ところが、この記事を書くために公式ドキュメントを読み直したところ、そのメモは書いた日から間違っていました。Plainfeedの最初のコミットより4か月以上前の2026年6月から、search.listは1万ユニットとは別の、1日100回の別枠を使っています。100ユニットだろうと1日100回だろうと、登録チャンネルのフィードをsearchで作れないという結論は同じです。

決定1:新しい動画はRSSで(0ユニット)

結論:新しい動画の一覧は無料のRSSで取得します。クォータはかかりません。

YouTubeにはチャンネルごとに公開のRSSフィードがあります。

https://www.youtube.com/feeds/videos.xml?channel_id=UC...

APIキーもクォータも要りません。公式ドキュメントで説明されているフィードではないので、以下は2026年10月に自分で取得して確かめた内容です。最新の動画15本のタイトル、投稿時刻、再生回数、リンクが入っています。リンクに/shorts/が含まれていればショートなので、長さがわからなくても除けます。今サーバーに取得してある動画590本のうち153本(26%)が、こうして無料で除けました。

ただし、RSSではわからないこともあります。

それに、ブラウザはYouTubeのRSSを直接読めないので(CORS)、同じサイトのサーバー関数(Cloudflare Pages Functions)がブラウザの代わりに取得します。APIキーもブラウザに置けないので、どのみちサーバーは必要でした。

決定2:APIはRSSでわからないことだけに使う

結論:動画の長さ、ライブの状態、チャンネルアイコン、コメントのようにRSSにないものだけをAPIに聞き、50件ずつまとめて1回1ユニットで済ませます。

使う場所 メソッド かかる量 頻度
動画の長さ・ライブの状態 videos.list 50件まで1回1ユニット 新しい動画が見つかったとき。取得した情報は7日、配信中なら5分
チャンネルアイコン channels.list 50件まで1ユニット チャンネルごとに7日に1回
コメント・返信 commentThreads.list、comments.list ページごとに1ユニット コメントタブを開いたときだけ。30分キャッシュ
URLでチャンネルを追加 channels.list(動画のURLならvideos.listも) 1〜2ユニット 追加するとき。同じURLは1日キャッシュ

videos.listは動画IDを一度に50件まで受け付けるので(リファレンスに数値の記載はありませんが、実際の上限です)、新しい動画50本を1ユニットで確認できます。長さが3分(180秒)以下ならショートとみなして除きます(ライブのアーカイブは短くても残します)。RSSのリンクで除けなかった短い動画が、こうしてさらに73本除けました。

// 取得済みの情報が古くなった動画だけ、50件ずつ聞く(1回1ユニット)。簡略化したコード
for (let i = 0; i < ids.length; i += 50) {
  const body = await api("videos", {
    part: "contentDetails,liveStreamingDetails,snippet,statistics",
    id: ids.slice(i, i + 50).join(","),
  });
  for (const v of body.items) {
    out[v.id] = {
      duration: parseDuration(v.contentDetails.duration),   // 180秒以下ならショート
      state: v.snippet.liveBroadcastContent,                 // live, upcoming, none
      start: v.liveStreamingDetails?.actualStartTime,       // ライブは投稿時刻の代わりに開始時刻を入れる
    };
  }
}

あえてしなかったこともあります。このサイトからすぐにチャンネル登録するボタンは作りませんでした。チャンネル登録の追加(subscriptions.insert)は1回50ユニットで、アカウントへの書き込み権限まで必要になるので、登録はYouTubeでしてもらいます。

決定3:チャンネルごとにキャッシュし、全員で共有する

結論:取得した結果はチャンネル単位で保存し、すべてのユーザーで共有します。ユーザーが増えてもコストはほとんど増えません。

取得した結果はCloudflare D1(SQLite)にチャンネルごとに1行で置きます。その1行の中に、RSSの結果と動画ごとの長さ・ライブの状態、チャンネルアイコンが、取得した時刻と一緒に入っています。

Plainfeedが新しい動画を取得する流れ。ブラウザがチャンネルIDをサーバー関数に送ると、サーバーはまずD1のキャッシュを読み、30分を過ぎたチャンネルだけRSSで取得し直し、RSSが教えてくれないことだけをYouTube Data APIに聞く。新しい動画が投稿されると、GoogleのWebSubハブが知らせてくれる。
まずキャッシュ、次にRSS、最後にAPI。①の箱の時間は取得し直す間隔です。

ポイントは、このキャッシュをユーザー同士で共有することです。フィードのリクエストにはチャンネルIDしか入っておらず、キャッシュはチャンネル単位なので、誰がリクエストしたかに関係なく同じ結果を返します。同じチャンネルを登録している人が1人でも1,000人でも、そのチャンネルの動画情報は1回しか取得しません。そのため、クォータはユーザー数ではなく、異なるチャンネルの数と、そのチャンネルが投稿する新しい動画の数に比例します。

APIで取得した情報は、30日のポリシーがあるので長く持っていられません。1日1回動くクリーンアップ処理が、30日以上取得し直せなかった行を削除し、次のリクエストのときにまた取得します。

決定4:新しい動画の通知(WebSub)で待たない

結論:30分ごとに確認する代わりに、新しい動画が投稿されたらGoogleが知らせてくれるようにしました。これも0ユニットです。

RSSを30分ごとに取得し直す方式だと、誰もサイトを開いていない間、サーバーは新しい動画を知りません。そこで、YouTube公式の新しい動画の通知であるWebSub(PubSubHubbub)を付けました。Googleのハブにチャンネルを購読登録しておくと、新しい動画が投稿されたときや、動画のタイトル・説明が変わったときにサーバーへ通知が届き、サーバーはそのチャンネルのRSSだけをすぐに取得し直します。これもクォータはかかりません。

通知は誰でも送れるURLに届くので、内容は信用しません。チャンネルごとに違う秘密の値で署名を確かめ、合っていればRSSを取得し直すだけです。通知が遅れたり抜けたりしたときのために、30分のキャッシュはそのまま残しています。

決定5:第三者が使える量に1日の上限

結論:ログインなしで呼べる機能ごとに、1日の使用量の上限をかけました。

ここまでは普通に使ったときの話です。ところがPlainfeedはログインなしで使えるので、フィード・チャンネル追加・コメントのAPIは誰でも呼べます。自分で点検してみると、チャンネルのURLやコメントの次ページのトークンを変えながら呼べば、数時間でクォータを使い切れる道がありました。

そこで、コストのかかる処理ごとに1日の予算をかけました。

処理 1日全体 リクエスト元ごと 超えたら
未取得のチャンネルの取得 1,000チャンネル 150チャンネル 取得できなかったチャンネルとして次回また
30分を過ぎたチャンネルの取得し直し 20,000チャンネル 2,000チャンネル 取得済みのものをそのまま
URLでチャンネルを探す 1,500回 100回 取得済みのものがなければ「本日の処理上限に達しました」
コメント・返信 3,000回 400回 〃

「リクエスト元」はIPをそのまま保存せず、日ごとに変わる秘密のハッシュでだけ区別しています。

全部足すと、コメントとチャンネル検索だけでも運の悪い日には1日4,500〜6,000ユニットまで使われる可能性があります。1万には収まりますが余裕が大きいわけではないので、ユーザーが増えたらまずこの数字から見直すつもりです。

クォータのほかにも、絞らなければならないものが2つありました。

try {
  feed = await fetchRss(id);                 // 0ユニット
} catch (e) {
  // 代替取得は以前取得したチャンネルだけ、1リクエストにつき6チャンネルまで、
  // 代替取得まで失敗したチャンネルは1時間休む。簡略化したコード
  if (!known || fallbacks >= 6 || now - failedAt < 3600) throw e;
  fallbacks++;
  feed = await fetchUploads(id);             // playlistItems.list:1ユニット
}

予想と違ったこと

1. クォータをいちばん使っていたのは「代替取得」でした。 直近7日間に使った701ユニットのうち309ユニット(44%)が、RSSが失敗してアップロード一覧で代替取得した分です。RSSは無料なので失敗もめったにないと思っていましたが、違いました。不思議なのは、この記事を書きながら同じ40チャンネルのRSSを自分のパソコンで取得すると、40チャンネルすべてすぐに取得できることです。クラウドのサーバーから呼んだときだけときどき弾かれるようですが、まだ確かめていない推測です。

2. なくなったチャンネルが、リクエストのたびに1ユニットずつ使っていました。 RSSが失敗し、代替取得も失敗し、次のリクエストでまた試す。チャンネルが削除されたり非公開になったりすると、これを延々と繰り返します。代替取得まで失敗したらその時刻を記録しておき、1時間はもう試さないように直しました(上のコードのfailedAt)。

3. 「50件で1ユニット」は最良のケースの話でした。 40チャンネルの新しい動画は1日25本ほどで、7日ごとに確認し直す動画(ショートのリンクではない約440本)を足しても、50件ずつ満杯にすれば1日2回で済みます。ところが、7日間でvideos.listは270回(1日約39回)呼ばれていました。新しい動画はふつう1〜2本ずつ現れ、配信中のライブ(5分)や配信予定を確認し直す処理まで混ざるので、実際にはほとんどの呼び出しにIDが数件しか入りません。呼び出しごとの理由は数えていないので、これも推測です。

4. 自分の設計メモが初日から間違っていました。 前に書いたsearch.listの話です。ルールは私がメモを書く数か月前にすでに変わっていました。数字の入った記事は、公開の直前にドキュメントを読み直すべきだと学びました。

実際に使った量

Google CloudコンソールのYouTube Data APIの指標から、2026年10月29日までの7日間のメソッド別リクエスト数を書き写しました。ここに出てくるメソッドはどれも1回1ユニットなので、リクエスト数がそのままユニットです。

メソッド 7日間(ユニット) 使う場所
playlistItems.list 309 RSSの代替取得
videos.list 270 動画の長さ・ライブの状態
commentThreads.list 58 コメント
channels.list 32 チャンネルアイコン、URLでチャンネルを追加
subscriptions.list 21 Googleアカウントで登録チャンネルを読み込む
comments.list 11 返信
合計 701 1日約100ユニット(上限の1%)

登録チャンネルの読み込みはユーザーのGoogleトークンでブラウザから呼んでいるのに、クォータはPlainfeedのプロジェクトから引かれることも、今回確かめました。

数字を読むときには条件があります。まだ正式リリース前なので実際のユーザーは私一人で、チャンネルは40、開発中に私が直接呼んだ分も混ざっています。人が増えれば数字も増えます。ただ、増える方向は決まっています。同じチャンネルを見る人が増えるのはほとんど無料で、新しいチャンネルとそのチャンネルの新しい動画が増えるときにコストがかかります。

同じ40チャンネルを基準に比べるとこうなります。

40チャンネルを見るときに1日にかかるユニットの比較。30分ごとにAPIで確認すると1,920ユニット、Plainfeedの実測は100ユニット、RSSの失敗がなく50件ずつ満杯で聞いたときの理論上の下限は約2ユニット。
実測はコメントと登録チャンネルの読み込みまで含んだ値です。

しなかったこと、次にすること

おわりに

クォータを節約するコツは、APIを呼ぶ回数を減らす小技よりも、APIに聞く必要のない作りにありました。新しい動画はRSSと通知で知り、APIはRSSが教えてくれないことを埋め、その結果は全員で共有する。計画どおりにいかなかったところ(RSSの代替取得)が、次に手を入れる場所です。

次の技術記事では、同じサーバーをCloudflareの無料プラン1つで運営しながら、D1の1日の読み書きの上限を他人に使い切られないようにした話を書くつもりです。

ブログの記事一覧