DEV Community

ahmet gedik
ahmet gedik

Posted on

Rate Limiting and API Key Management for Video Data APIs

YouTube's Data API v3 quota is 10,000 units per key per day. For ViralVidVault, which fetches trending videos across seven European regions every seven hours, that quota needs careful management. One careless loop can burn through an entire day's allocation in minutes.

The Quota Math

Understand what each call costs before writing any fetch logic:

API Call Quota Cost
search.list 100 units
videos.list 1 unit
channels.list 1 unit
playlistItems.list 1 unit

A naive approach — searching each category in each region — means 7 regions times 15 categories times 100 units = 10,500 units per fetch cycle. That's more than one key allows per day.

Key Rotation Architecture

Multiple API keys, rotated by usage. Store usage data in the same SQLite database that holds video data:

class YouTubeKeyManager {
    private PDO $db;

    public function __construct(PDO $db) {
        $this->db = $db;
        $this->resetExpiredKeys();
    }

    private function resetExpiredKeys(): void {
        // Google resets quotas at midnight Pacific Time
        $this->db->exec(
            "UPDATE api_keys 
             SET daily_usage = 0, last_reset = datetime('now')
             WHERE last_reset < datetime('now', '-1 day')"
        );
    }

    public function getBestKey(): ?array {
        $stmt = $this->db->query(
            "SELECT id, api_key, daily_usage 
             FROM api_keys 
             WHERE active = 1 AND daily_usage < 9500
             ORDER BY daily_usage ASC 
             LIMIT 1"
        );
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    public function recordUsage(int $keyId, int $units): void {
        $stmt = $this->db->prepare(
            "UPDATE api_keys 
             SET daily_usage = daily_usage + ?,
                 total_usage = total_usage + ?,
                 last_used = datetime('now')
             WHERE id = ?"
        );
        $stmt->execute([$units, $units, $keyId]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The getBestKey() method selects the key with the lowest daily usage, spreading the load evenly. The 9,500-unit threshold leaves a 500-unit buffer to prevent accidental overruns.

Rate Limiter Class

Google enforces per-second limits in addition to daily quotas. A simple token bucket keeps you under the wire:

class TokenBucketLimiter {
    private float $tokens;
    private float $maxTokens;
    private float $refillRate;
    private float $lastRefill;

    public function __construct(float $requestsPerSecond = 3.0) {
        $this->maxTokens = $requestsPerSecond;
        $this->tokens = $requestsPerSecond;
        $this->refillRate = $requestsPerSecond;
        $this->lastRefill = microtime(true);
    }

    public function acquire(): void {
        $this->refill();
        while ($this->tokens < 1.0) {
            usleep(50000); // 50ms
            $this->refill();
        }
        $this->tokens -= 1.0;
    }

    private function refill(): void {
        $now = microtime(true);
        $elapsed = $now - $this->lastRefill;
        $this->tokens = min($this->maxTokens, $this->tokens + $elapsed * $this->refillRate);
        $this->lastRefill = $now;
    }
}
Enter fullscreen mode Exit fullscreen mode

Unlike a simple sleep(1) between calls, the token bucket allows bursts up to the limit and then throttles smoothly.

Quota-Efficient Fetch Strategy

The biggest win is avoiding search.list entirely when possible. The videos.list endpoint with chart=mostPopular costs just 1 unit:

function fetchRegionContent(
    YouTubeKeyManager $km,
    TokenBucketLimiter $limiter,
    string $region
): array {
    $results = [];

    // 1 unit per call instead of 100
    $key = $km->getBestKey();
    $limiter->acquire();

    $popular = youtubeGet('videos', [
        'part'       => 'snippet,statistics',
        'chart'      => 'mostPopular',
        'regionCode' => $region,
        'maxResults' => 50,
        'key'        => $key['api_key'],
    ]);
    $km->recordUsage($key['id'], 1);
    $results = array_merge($results, $popular['items'] ?? []);

    // Per-category popular (also 1 unit each)
    $categories = ['1', '2', '10', '17', '20', '22', '23', '24', '25', '26', '28'];
    foreach ($categories as $catId) {
        $key = $km->getBestKey();
        if (!$key) break;

        $limiter->acquire();
        $catVideos = youtubeGet('videos', [
            'part'            => 'snippet,statistics',
            'chart'           => 'mostPopular',
            'regionCode'      => $region,
            'videoCategoryId' => $catId,
            'maxResults'      => 50,
            'key'             => $key['api_key'],
        ]);
        $km->recordUsage($key['id'], 1);
        $results = array_merge($results, $catVideos['items'] ?? []);
    }

    return $results;
}
Enter fullscreen mode Exit fullscreen mode

For 7 regions with 11 categories each, that's 7 * 12 = 84 units per fetch cycle. Compare that to 10,500 units with the search.list approach. A 125x improvement.

Handling Quota Errors Gracefully

When a key hits its limit, YouTube returns a 403 with a specific error reason. Catch it and rotate:

function youtubeGetWithRetry(
    YouTubeKeyManager $km,
    TokenBucketLimiter $limiter,
    string $endpoint,
    array $params
): ?array {
    $attempts = 0;
    $maxAttempts = 3;

    while ($attempts < $maxAttempts) {
        $key = $km->getBestKey();
        if (!$key) {
            error_log('All YouTube API keys exhausted');
            return null;
        }

        $params['key'] = $key['api_key'];
        $limiter->acquire();
        $response = httpGet(buildYouTubeUrl($endpoint, $params));

        if ($response['status'] === 200) {
            return json_decode($response['body'], true);
        }

        if ($response['status'] === 403) {
            $body = json_decode($response['body'], true);
            $reason = $body['error']['errors'][0]['reason'] ?? 'unknown';
            if (in_array($reason, ['quotaExceeded', 'dailyLimitExceeded'])) {
                $km->recordUsage($key['id'], 10000); // Mark as exhausted
                $attempts++;
                continue;
            }
        }

        error_log("YouTube API error: HTTP {$response['status']}");
        return null;
    }
    return null;
}
Enter fullscreen mode Exit fullscreen mode

Monitoring Dashboard

Track quota consumption to catch issues before they become outages:

function getQuotaDashboard(PDO $db): array {
    $keys = $db->query(
        "SELECT api_key, daily_usage, last_used, last_reset,
                ROUND(daily_usage * 100.0 / 10000, 1) AS pct
         FROM api_keys WHERE active = 1
         ORDER BY daily_usage DESC"
    )->fetchAll(PDO::FETCH_ASSOC);

    $totalUsed = array_sum(array_column($keys, 'daily_usage'));
    $totalCap = count($keys) * 10000;

    return [
        'total_used'     => $totalUsed,
        'total_capacity' => $totalCap,
        'pct_used'       => round($totalUsed / $totalCap * 100, 1),
        'keys'           => $keys,
    ];
}
Enter fullscreen mode Exit fullscreen mode

On ViralVidVault, the admin panel displays this in real time. When aggregate usage crosses 70%, the system logs a warning. At 90%, it reduces fetch frequency to preserve quota for the rest of the day.

Key Takeaway

The YouTube API quota is not a problem to solve once — it's a constraint to design around from the start. Choose cheap endpoints over expensive ones, rotate keys evenly, and monitor consumption. The difference between a platform that runs smoothly and one that goes dark at 3pm because it burned through its quota is just a few hundred lines of management code.


This article is part of the Building ViralVidVault series.

Top comments (0)