YouTube API Quota
Lumio uses InnerTube - YouTube's own internal API - as the primary transport for chat reception and broadcast discovery. InnerTube costs 0 Data API quota. The YouTube Data API v3 resolves the liveChatId of newly discovered broadcasts, and is also used for chat sending, moderation, channel-info lookup at connection time, and the disabled-by-default REST polling fallback.
The YouTube Data API v3 enforces a daily quota of 10,000 units per project (resets at midnight Pacific Time).
Transport Overview
| Transport | Purpose | Quota Cost |
|---|---|---|
| InnerTube (primary) | Chat polling, broadcast discovery, badge/emote enrichment, statistics (likes, views) | 0 |
| gRPC streamList (fallback 1) | Chat reception via persistent server-push connection | 0 |
| REST Data API v3 (fallback 2) | Chat polling when InnerTube and gRPC both fail | 5 units per poll |
Fallback cascade: InnerTube → gRPC (if grpc_fallback_enabled) → REST (if rest_fallback_enabled). Each transition triggers after 3 consecutive failures within 60 seconds. Both fallbacks are disabled by default.
Quota Cost Reference
Official costs per YouTube Data API v3 operation:
| Operation | Endpoint | Cost (units) |
|---|---|---|
| List broadcasts | liveBroadcasts.list | 1 |
| List chat messages | liveChatMessages.list | 5 |
| Send chat message | liveChatMessages.insert | 200 |
| List channels | channels.list | 1 |
| List videos | videos.list | 1 |
| Search | search.list | 100 |
| List subscriptions | subscriptions.list | 1 |
| Transition broadcast | liveBroadcasts.transition | 50 |
The default daily limit is 10,000 units. Higher limits require passing Google's Quota and Compliance Audit.
Quota-Free Operations (InnerTube + gRPC)
These operations cost 0 Data API quota:
| Operation | Transport | Source |
|---|---|---|
| Chat polling | InnerTube get_live_chat | crates/lo-youtube-api/src/innertube/mod.rs |
| Broadcast discovery | InnerTube browse | crates/lo-youtube-api/src/innertube/browse.rs |
| Badge + emote enrichment | InnerTube (inline in chat responses) | crates/lo-youtube-api/src/innertube/parser.rs |
| Like count + view count | InnerTube updated_metadata / player | crates/lo-youtube-api/src/innertube/browse.rs |
| Like count + view count (fallback) | Public channel Atom feed videos.xml (media:community) | crates/lo-youtube-api/src/webhook.rs |
| Chat streaming (fallback) | gRPC liveChatMessages.streamList | crates/lo-youtube-api/src/streaming.rs |
InnerTube is unauthenticated - no OAuth token required for chat reception. The channel Atom feed is likewise unauthenticated, but is edge-cached for 15 minutes, so it is fetched at most once per channel per 15 minutes and only for values InnerTube did not deliver. See Broadcast Statistics.
Broadcast Discovery (Quota-Free)
The InnerTube browse request reads a channel's public Streams tab. Current entries use lockupViewModel: a …_LIVE badge style identifies a live broadcast, while badge text Upcoming identifies a scheduled one. The request pins hl=en so the badge text is stable and sets timeZone=UTC and utcOffsetMinutes=0 so the Scheduled for … time can be parsed as UTC. A legacy videoRenderer parser remains as a fallback. InnerTube returns the video ID, title, and available status metadata, but no liveChatId.
Only public streams appear in this discovery path. Unlisted and private broadcasts are not discovered through the public Streams tab.
liveChatId Resolution (Data API v3)
| Aspect | Details |
|---|---|
| Endpoint | liveBroadcasts.list?part=snippet,status&id=…&maxResults=50 |
| Cost | 1 unit per call, with up to 50 video IDs per call |
| Frequency | On discovery when a broadcast has no known chat ID; unresolved IDs are retried after at least 10 minutes |
| Source | crates/lo-youtube-api/src/client.rs get_broadcasts_by_id() and apps/api/src/workers/youtube.rs |
The worker reuses known chat IDs from tracked streams or channel_status. For an unknown ID, it calls the Data API with the connected channel's OAuth token. snippet.scheduledStartTime, when returned, takes precedence over the time parsed from the Streams tab. An upcoming broadcast without a chat ID still appears on the dashboard, but it has no chat reader task and does not appear in the Multichat broadcast dropdown until the ID resolves. A live broadcast without a chat ID waits for resolution. A failed lookup leaves broadcasts with known IDs available; quota errors pause further lookups for five minutes, and other API errors for one discovery interval (60 seconds).
The live-chat embed page is still used to bootstrap an InnerTube chat continuation token after a chat ID is known. It is not the source of the liveChatId.
Quota-Consuming Operations (Data API v3)
Chat Sending + Moderation
| Operation | Endpoint | Cost | Frequency | Source |
|---|---|---|---|---|
| Send chat message | POST /liveChat/messages | 200 | Per message sent | apps/api/src/services/chat_send.rs |
| Ban / timeout user | POST /liveChat/bans | 200 | Per action | apps/api/src/services/moderation.rs |
| Delete message | DELETE /liveChat/messages | 200 | Per action | apps/api/src/services/moderation.rs |
These use the account's Google login OAuth connection (db::auth::get_account_provider_token), not the channel connection.
Channel Info Lookup
| Aspect | Details |
|---|---|
| Endpoint | channels.list |
| Cost | 1 unit |
| Source | apps/api/src/routes/connections.rs fetch_channel_info() |
| Trigger | Once per YouTube channel connection (OAuth callback) |
| Daily Units | Negligible (< 10) |
REST Fallback (Disabled by Default)
Broadcast discovery through the Data API runs when rest_fallback_enabled = true and InnerTube browse fails. REST chat polling is the last transport after the enabled InnerTube and gRPC tiers fail:
| Operation | Endpoint | Cost | Interval |
|---|---|---|---|
| Broadcast discovery | liveBroadcasts.list | At least 2 (one page each for active and upcoming) | Every 60 s; more for additional pages |
| Chat polling | liveChatMessages.list | 5 | ~10 s active / 30 s idle |
| Caller | File | Daily Units (per channel, one page per broadcast status) |
|---|---|---|
| YouTube Worker (broadcast poll) | apps/api/src/workers/youtube.rs | At least 2,880 |
| YouTube Worker (chat poll) | apps/api/src/workers/youtube.rs | up to 43,200 |
Configuration
[youtube]
# Enable gRPC streamList as first fallback when InnerTube fails
# ENV: LUMIO__YOUTUBE__GRPC_FALLBACK_ENABLED
grpc_fallback_enabled = false # default
# Enable REST polling as last resort when InnerTube + gRPC both fail
# Also gates Data API broadcast discovery fallback
# ENV: LUMIO__YOUTUBE__REST_FALLBACK_ENABLED
rest_fallback_enabled = false # default
Daily Quota Calculator
Default Mode (InnerTube Only)
With both fallbacks disabled (default), quota consumption is minimal:
Per active channel (stream is live):
| Component | Quota per call | Calls | Quota |
|---|---|---|---|
| InnerTube chat polling | 0 | ~28,800/day (3 s interval) | 0 |
| InnerTube broadcast discovery | 0 | 1,440/day (60 s interval) | 0 |
| InnerTube statistics | 0 | 1,440/day (60 s interval) | 0 |
liveChatId resolution (liveBroadcasts.list?id=…) | 1 per call | 1 call for up to 50 new IDs; unresolved IDs retry after at least 10 min | 1 for one resolved broadcast |
| Total for one resolved broadcast | 1 |
Per idle channel (no stream):
| Component | Quota per call | Calls | Quota |
|---|---|---|---|
| InnerTube broadcast discovery | 0 | 1,440/day | 0 |
| Total | 0 |
Example: 1 Channel, 8h Stream (Default InnerTube Mode)
Idle hours (16h): 0 units
Stream hours (8h): 0 units
liveChatId resolution (once): 1 unit
─────────
Total: 1 unit / 10,000 limit
This example assumes the chat ID resolves on the first call. A broadcast without a chat ID may cost another unit on each retry after at least 10 minutes. Human actions also consume quota - sending a chat message (200), a ban/timeout (200), or a message delete (200) - as does the one-off channels.list at connection time (1).
Example: 1 Channel, 8h Stream (REST Fallback Active)
Idle hours (16h): 16 * 120 = 1,920 units (active + upcoming discovery)
Stream hours (8h): 8 * 1920 = 15,360 units (discovery + chat polling)
─────────
Total: 17,280 units / 10,000 limit (173% -- OVER QUOTA)
This assumes one page for each broadcast status and a chat poll every 10 seconds. The fallback listing supplies the chat ID, so this continuous-fallback example has no separate ID lookup. REST fallback is disabled by default for this reason.
Quota Safeguards
| Mechanism | Details |
|---|---|
| InnerTube primary | 0 quota for all chat reception and broadcast discovery |
| Detection | HTTP 403 (REST) or RESOURCE_EXHAUSTED (gRPC) |
| Backoff | 5-minute pause on quota exhaustion (QUOTA_BACKOFF_SECS = 300) |
| Fallback chain | InnerTube → gRPC → REST (each after 3 failures in 60 s) |
| REST toggle | rest_fallback_enabled = false disables REST fallback entirely |
| Shared cache | Copyright Worker and Bot read broadcast status from Redis, not YouTube API |
| Adaptive polling | REST fallback respects polling_interval_millis from YouTube's response |
Requesting Higher Quota
To request a quota increase from Google:
- Pass the Quota and Compliance Audit
- Demonstrate compliance with YouTube API Terms of Service
- If previously audited within 12 months, use the Audited Developer Requests Form
- There is no published maximum - increases are granted case-by-case