YouTube API Quota
Multi-Key-Pool-System mit automatischem Failover, Exhaustion-Caching und Recovery fuer die YouTube Data API v3.
Key Pool System
Der YouTubeDataApiClient verwaltet mehrere API-Key-Pools mit einer definierten Failover-Kaskade. Jeder Pool hat eine eigene Prioritaetsreihenfolge, faellt aber automatisch auf die allgemeinen Keys zurueck.
Failover-Kaskade
Pool 'video': YOUTUBE_API_VIDEO_KEYS -> YOUTUBE_API_EXTENDED_KEYS -> YOUTUBE_API_KEYS -> YOUTUBE_API_KEY
Pool 'research': YOUTUBE_API_RESEARCH_KEYS -> YOUTUBE_API_KEYS -> YOUTUBE_API_KEY
Pool 'default': YOUTUBE_API_KEYS -> YOUTUBE_API_KEY
Keys werden automatisch dedupliziert -- ein Key der in mehreren Pools vorkommt, wird nur einmal pro Kaskade versucht.
Pool-Zuordnung
| Pool | Verwendung | Primary Keys |
|---|---|---|
default | Channel-Import, Daily Scrape, allgemeine Abfragen | YOUTUBE_API_KEYS |
video | Video-Sync (playlistItems, videos.list) | YOUTUBE_API_VIDEO_KEYS |
research | Related Channels Suche (search.list) | YOUTUBE_API_RESEARCH_KEYS |
extended | Legacy-Pool | YOUTUBE_API_EXTENDED_KEYS |
Location: app/Services/Social/YouTube/YouTubeDataApiClient.php (Methode keysForPool())
Key Rotation Engine
Die Rotation arbeitet mit einem Two-Pass-Ansatz:
Pass 1: Nicht-erschoepfte Keys durchprobieren
fuer jeden Key im Pool:
- Skip falls Key im Exhaustion-Cache liegt
- API-Request ausfuehren
- Bei Erfolg: Key verwenden, fertig
- Bei Quota/Rate-Limit-Fehler: Key als exhausted markieren, naechsten Key versuchen
- Bei einem Fehler, der dem KEY gehoert: naechsten Key versuchen (siehe unten)
- Bei einem Fehler, der der ANFRAGE gehoert: sofort Exception werfen
Drei Fehlerklassen, nicht zwei
Bis zum 02.09.2026 kannte die Rotation nur „Kontingent erschöpft" (nächsten Key probieren) und „alles andere" (sofort aufgeben). Damit entschied die Antwort eines Keys über den ganzen Aufruf, obwohl weitere im Pool bereitlagen und Pass 2 nie erreicht wurde.
Die entscheidende Frage ist: Kann ein anderer Key eine andere Antwort geben?
| Klasse | Beispiele | Reaktion |
|---|---|---|
| Kontingent | quotaExceeded, dailyLimitExceeded, rateLimitExceeded, userRateLimitExceeded | Key als exhausted markieren, nächsten probieren |
| Gehört dem Key | forbidden, keyInvalid, keyExpired, ipRefererBlocked, accessNotConfigured | Nächsten Key probieren, nicht als exhausted markieren |
| Gehört der Anfrage | invalidParameter, badRequest, videoNotFound … | Sofort abbrechen — mit jedem Key gleich fehlerhaft, ein Wiederholungsversuch verbrennt nur Kontingent |
forbidden steht bewusst in der mittleren Klasse, obwohl es mehrdeutig ist. Google liefert für videos.list unter diesem Grund einen Standardtext über den myRating-Parameter — auch dann, wenn die Anfrage diesen Parameter gar nicht enthält. Der Text sagt also nichts darüber, was wirklich vorlag. Es einmal mit dem nächsten Key zu versuchen kostet einen Aufruf und beantwortet die Frage; es nicht zu versuchen hat einen ganzen Sync-Lauf gekostet.
Wenn JEDER Key ablehnt
Dann wirft der Client YouTubeKeyRejectedException — nicht YouTubeQuotaExceededException, und das ist der Unterschied, auf den es ankommt:
- Ein erschöpftes Kontingent heilt sich zum nächsten Reset von selbst.
- Ein abgelehnter Key heilt sich nie und braucht einen Menschen.
Die Ausnahme trägt Pool, Endpunkt, die Ablehnungsgründe und die Zahl der versuchten Keys — den Key selbst bewusst nicht, denn die Meldung landet in zwei externen Fehler-Diensten.
⚠️ Eine Ablehnung öffnet den Circuit Breaker NICHT. Der sperrt den Pool bis zum nächsten Quota-Reset; für ein Problem, das mit dem Kontingent nichts zu tun hat, wäre das die falsche Antwort und würde jeden weiteren Aufruf stundenlang blockieren.
SyncYouTubeVideoStats behandelt sie wie den Kontingent-Fall: loggen, Metrik zählen, ohne Arbeit beenden — und keinen Fehlschlag beim Profil buchen. Das ist wichtig, weil der Job nach MAX_CONSECUTIVE_FAIL_DAYS = 7 Fehlschlägen die Synchronisierung des Profils abschaltet. Ein Konfigurationsfehler an einem einzigen Key hätte sonst reihenweise Profile stillgelegt — und zwar genau die, die am häufigsten synchronisieren.
Pass 2: Recovery-Versuch
falls alle Keys exhausted:
- 3 Sekunden warten (RECOVERY_DELAY_SECONDS)
- Keys erneut versuchen die in DIESEM Call exhausted wurden
- Rate-Limits erholen sich oft innerhalb von Sekunden
- Bei Erfolg: Exhaustion-Cache fuer diesen Key loeschen
Exhaustion-Cache
Erschoepfte Keys werden im Redis-Cache markiert, damit nachfolgende Requests sie ueberspringen:
| Fehlertyp | Cache TTL | Grund |
|---|---|---|
quotaExceeded, dailyLimitExceeded | Bis Mitternacht PT + 5 Min (min 30 Min) | Tages-Quota erholt sich erst um Mitternacht Pacific Time |
rateLimitExceeded, userRateLimitExceeded | 5 Minuten | Rate-Limits erholen sich schnell |
// Cache-Key-Format
'yt_key_exhausted:{pool}:{md5_prefix_8chars}'
Location: app/Services/Social/YouTube/YouTubeDataApiClient.php (Methoden markKeyExhausted(), isKeyExhausted(), clearKeyExhausted())
Circuit Breaker
Der YouTubeQuotaGuard implementiert einen Circuit-Breaker pro Pool, der API-Calls blockiert wenn alle Keys erschoepft sind.
Ablauf
canMakeCall($pool)prueft zuerst den Circuit Breaker (guenstig, pool-weit)- Wenn Circuit Breaker offen → pruefe ob einzelne Keys recovered sind
- Wenn ein Key recovered → Circuit Breaker schliessen, Call erlauben
- Wenn kein Key recovered → Call blockieren
Circuit Breaker TTL
| Ausloeser | TTL | Beschreibung |
|---|---|---|
| Daily Quota Exhaustion | Bis Mitternacht PT + 5 Min (min 30 Min) | Dynamisch berechnet via secondsUntilQuotaReset() |
| Rate Limit | 5 Minuten | Keys erholen sich schnell |
TTL-Berechnung (Mitternacht Pacific Time)
YouTube-Quotas resetten um Mitternacht Pacific Time (UTC 08:00 PST / UTC 07:00 PDT). Die TTL wird berechnet als endOfDay('America/Los_Angeles') + 6 Minuten, mit einem Safety Floor von 30 Minuten.
// Cache-Key-Format
'yt_pool_circuit_breaker:{pool}'
Location: app/Services/Social/YouTube/YouTubeQuotaGuard.php
Video-Sync (Full-Sync)
Der Video-Sync fuehrt taeglich einen vollstaendigen Playlist-Fetch durch (14:00 UTC). Delta-Sync-Spalten (last_seen_video_id, last_video_count etc.) wurden entfernt und durch einen einfacheren Full-Sync-Ansatz ersetzt.
Neue Videos werden primaer durch WebSub Push-Notifications und RSS-Feed-Polling erkannt (0 API-Quota). Der taeglich Full-Sync gleicht lediglich Statistiken ab.
Quota-Kosten pro Operation
| Operation | Endpoint | Quota-Kosten |
|---|---|---|
| Channel abrufen | channels.list | 1 Unit |
| Playlist-Items | playlistItems.list | 1 Unit |
| Video-Details | videos.list | 1 Unit |
| Channel-Suche | search.list | 100 Units |
Das taegliche Quota-Limit pro Projekt betraegt standardmaessig 10.000 Units. Ein einzelner search.list-Call verbraucht somit 1% des Tagesbudgets.
Typische Kosten-Szenarien
| Szenario | Units pro Profil |
|---|---|
| Daily Scrape (Channel-Refresh) | 1 Unit |
| Video Full-Sync (taeglich, bekannte Videos) | 2-3 Units |
| Video Full-Sync (mit neuen Videos) | 3-4 Units |
| Related Channels Suche | 100-500 Units |
Error-Benachrichtigung bei Final Fallback
Wenn die Kaskade auf den letzten Key (YOUTUBE_API_KEY) zurueckfaellt, wird ein Error-Report ausgeloest:
report(new RuntimeException(
"YouTube API Quota exhausted for pool '{$pool}', falling back to YOUTUBE_API_KEY"
));
Rate-Limited auf 1x pro Stunde pro Pool via Cache-Key youtube_api_fallback_notified:{pool}.
Location: app/Services/Social/YouTube/YouTubeDataApiClient.php (Methode notifyIfFinalFallback())
Research Quota Service
Der ResearchQuotaService prueft die verfuegbare Quota fuer postbox-research-* Projekte, bevor Auto-Fill-Jobs gestartet werden.
$service = app(ResearchQuotaService::class);
$status = $service->getStatus();
// -> ['used' => 4500, 'limit' => 10000, 'remaining' => 5500, 'percent_available' => 55.0, ...]
$service->allowsAutoFill(); // true wenn >= 25% verfuegbar
- Gecached fuer 5 Minuten
- Aggregiert Quota ueber alle
postbox-research-*Projekte - Liest aus
google_api_quota_limitsundgoogle_api_quota_usagesTabellen
Location: app/Services/YouTube/ResearchQuotaService.php
Connection Retry
Der HTTP-Client implementiert Retry-Logik fuer Verbindungsfehler (SSL-Timeouts, Connection-Resets):
- Max 3 Versuche
- Exponential Backoff: 2s, 4s, 8s
- Timeout: 30s Request, 15s Connect
- Nur bei
ConnectionException(nicht bei API-Fehlern)
.env Variablen
| Variable | Default | Beschreibung |
|---|---|---|
YOUTUBE_API_KEY | -- | Single Fallback Key (letzte Kaskadenstufe) |
YOUTUBE_API_KEYS | -- | General Pool, kommasepariert |
YOUTUBE_API_EXTENDED_KEYS | -- | Extended Pool (Legacy), kommasepariert |
YOUTUBE_API_VIDEO_KEYS | -- | Dedizierter Pool fuer Video-Sync |
YOUTUBE_API_RESEARCH_KEYS | -- | Dedizierter Pool fuer Related Channels / search.list |
Alle Key-Variablen akzeptieren kommaseparierte Listen (AIza...,AIza...,AIza...).