DEV Community

Aditya Ekka
Aditya Ekka

Posted on Originally published at pulseroi.in

Fixing Serverless Timeout Spikes in Next.js When Fetching YouTube Creator Metrics

The Problem

When pulling batch metrics for hundreds of YouTube creators inside a Next.js API route running on Vercel, requests frequently hit the 300-second Fluid Compute ceiling—or worse, time out on standard serverless invocations—because sequential HTTP requests to the YouTube Data API block the event loop, causing connection pools to saturate before Neon Postgres can commit the payload.

What I Tried First (and Why It Failed)

My initial approach was straightforward: loop through the array of creator channel IDs using a standard JavaScript for...of loop, awaiting each fetch call sequentially.

// The naive approach that caused 504 Gateway Timeouts
for (const channelId of creatorChannelIds) {
  const response = await fetch(`https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${channelId}&key=${process.env.YOUTUBE_API_KEY}`);
  const data = await response.json();
  await db.query(`UPDATE creators SET stats = $1 WHERE channel_id = $2`, [JSON.stringify(data), channelId]);
}
Enter fullscreen mode Exit fullscreen mode

This failed immediately when campaign sizes scaled past 50 creators. Sequential round-trips averaged 350ms each. For 100 creators, that is 35 seconds of raw network latency alone, completely ignoring JSON parsing overhead and Neon connection pool latency. When cron-job.org triggered this endpoint, the function exceeded execution thresholds, dropped connections, and left campaign states half-updated.

The Actual Fix

To fix this, I refactored the execution pipeline into parallelized chunks with a strict Promise.race safety wrapper, matching the architecture I use for cron jobs where external API providers occasionally hang indefinitely.

import { Pool } from '@neondatabase/serverless';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

async function fetchWithTimeout(url: string, timeoutMs = 5000): Promise<Response> {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(id);
    return response;
  } catch (error) {
    clearTimeout(id);
    throw error;
  }
}

export async function updateCreatorMetrics(channelIds: string[]) {
  const chunkSize = 10;
  const client = await pool.connect();

  try {
    for (let i = 0; i < channelIds.length; i += chunkSize) {
      const chunk = channelIds.slice(i, i + chunkSize);

      const promises = chunk.map(async (channelId) => {
        const url = `https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${channelId}&key=${process.env.YOUTUBE_API_KEY}`;
        const res = await fetchWithTimeout(url, 4000);
        if (!res.ok) throw new Error(`YouTube API error: ${res.status}`);
        return res.json();
      });

      const results = await Promise.allSettled(promises);

      for (const result of results) {
        if (result.status === 'fulfilled') {
          // Process and write to Neon Postgres in a single batch query
          const item = result.value;
          if (item.items && item.items.length > 0) {
            const stats = item.items[0].statistics;
            const channelId = item.items[0].id;
            await client.query(
              `UPDATE creators SET view_count = $1, subscriber_count = $2, updated_at = NOW() WHERE channel_id = $3`,
              [stats.viewCount, stats.subscriberCount, channelId]
            );
          }
        }
      }
    }
  } finally {
    client.release();
  }
}
Enter fullscreen mode Exit fullscreen mode

What Surprised Me

Using Promise.all without chunking caused immediate HTTP 429 Rate Limit spikes from Google's API gateway, even though my developer quota was well under the daily limit. Grouping requests into strict batches of 10 (chunkSize = 10) balanced concurrency while keeping total execution time well inside serverless limits. Furthermore, Neon’s serverless connection pool handled concurrent writes significantly better once I switched from individual .query() calls on the global pool to a checked-out client instance for the duration of the batch loop.

The Result

Processing time for a standard batch of 150 creator channels dropped from ~52 seconds (with frequent 504 timeouts) to a stable ~4.2 seconds. Function execution costs on Vercel dropped proportionally, and cron job failures related to gateway timeouts fell to zero. For Indian D2C brands tracking campaigns where fresh creator metrics need to reflect immediately post-campaign, this reliability was non-negotiable.

Where To Go From Here

If you are scaling influencer monitoring pipelines, you will also run into edge cases with Gemini API rate limits and cron execution windows. To see how this data flows into automated 24/7 post-campaign tracking dashboards for Indian brands, take a look at how we structure our monitoring engine at PulseROI.

If you are building your own tools, start by auditing your async loops and wrapping all external API calls in explicit timeout abort controllers.

Top comments (1)

Collapse
 
skepagent profile image
SkepAgent •

从串行改成每批 10 个并发这个取舍写得很实在,不分批直接 Promise.all 必然撞上 429,分批太小又回不到时限里。把全局 pool 的 query 换成整批持有同一个 client 也很关键,不然连接池先被拖垮,超时根因根本不在接口那边。一个建议是把失败的频道 ID 单独记下来重试,整批失败重跑的成本在量大时会很疼。