Zum Hauptinhalt springen

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​

PoolVerwendungPrimary Keys
defaultChannel-Import, Daily Scrape, allgemeine AbfragenYOUTUBE_API_KEYS
videoVideo-Sync (playlistItems, videos.list)YOUTUBE_API_VIDEO_KEYS
researchRelated Channels Suche (search.list)YOUTUBE_API_RESEARCH_KEYS
extendedLegacy-PoolYOUTUBE_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?

KlasseBeispieleReaktion
KontingentquotaExceeded, dailyLimitExceeded, rateLimitExceeded, userRateLimitExceededKey als exhausted markieren, nächsten probieren
Gehört dem Keyforbidden, keyInvalid, keyExpired, ipRefererBlocked, accessNotConfiguredNächsten Key probieren, nicht als exhausted markieren
Gehört der AnfrageinvalidParameter, 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:

FehlertypCache TTLGrund
quotaExceeded, dailyLimitExceededBis Mitternacht PT + 5 Min (min 30 Min)Tages-Quota erholt sich erst um Mitternacht Pacific Time
rateLimitExceeded, userRateLimitExceeded5 MinutenRate-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​

  1. canMakeCall($pool) prueft zuerst den Circuit Breaker (guenstig, pool-weit)
  2. Wenn Circuit Breaker offen → pruefe ob einzelne Keys recovered sind
  3. Wenn ein Key recovered → Circuit Breaker schliessen, Call erlauben
  4. Wenn kein Key recovered → Call blockieren

Circuit Breaker TTL​

AusloeserTTLBeschreibung
Daily Quota ExhaustionBis Mitternacht PT + 5 Min (min 30 Min)Dynamisch berechnet via secondsUntilQuotaReset()
Rate Limit5 MinutenKeys 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​

OperationEndpointQuota-Kosten
Channel abrufenchannels.list1 Unit
Playlist-ItemsplaylistItems.list1 Unit
Video-Detailsvideos.list1 Unit
Channel-Suchesearch.list100 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​

SzenarioUnits 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 Suche100-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_limits und google_api_quota_usages Tabellen

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​

VariableDefaultBeschreibung
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...).