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 is 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 |
liveChatId resolution | Live-chat embed page scrape | crates/lo-youtube-api/src/innertube/scraper.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.
liveChatId Resolution (Quota-Free)
| Aspect | Details |
|---|---|
| Transport | Live-chat embed page scrape (fetch_live_chat_embed_page) |
| Cost | 0 — no Data API call |
| Frequency | Once per newly discovered broadcast, cached in channel_status |
| Source | crates/lo-youtube-api/src/innertube/browse.rs resolve_broadcast_chat_ids() |
InnerTube browse returns broadcast IDs but not liveChatId. resolve_broadcast_chat_ids() fetches each broadcast's live-chat embed page and extracts the id with extract_live_chat_id_from_html() — no liveBroadcasts.list call. Broadcasts whose chat id cannot be resolved (for example an upcoming stream with no chat room yet) are skipped rather than failing the poll.
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)
Only active when both rest_fallback_enabled = true and InnerTube + gRPC have failed:
| Operation | Endpoint | Cost | Interval |
|---|---|---|---|
| Broadcast discovery | liveBroadcasts.list | 1 | Every 60 s |
| Chat polling | liveChatMessages.list | 5 | ~10 s active / 30 s idle |
| Caller | File | Daily Units (per channel, worst case) |
|---|---|---|
| YouTube Worker (broadcast poll) | apps/api/src/workers/youtube.rs | 1,440 |
| 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 (embed-page scrape) | 0 | 1 per broadcast (cached) | 0 |
| Total | 0 |
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): 0 units
─────────
Total: 0 units / 10,000 limit
The only Data API spend in default mode comes from actions a human takes — sending a chat message (200), a ban/timeout (200), a message delete (200) — plus the one-off channels.list at connection time (1).
Example: 1 Channel, 8h Stream (REST Fallback Active)
Idle hours (16h): 16 * 60 = 960 units (broadcast poll)
Stream hours (8h): 8 * 1860 = 14,880 units (broadcast + chat poll)
─────────
Total: 15,840 units / 10,000 limit (158% -- OVER QUOTA)
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