在 Next.js 无服务器定时任务中轮换 Gemini API 密钥,避免触发配额上限

Dev.to AI 2026-10-31T03:58:25.430073

问题

在 Vercel 上通过定时后台任务自动分析 YouTube 创作者数据时,一次高并发的批处理就可能在不到两分钟内耗尽免费套餐的 AI API 额度。在 PulseROI,我们用 Gemini 3.8 Flash 跑持续性的后台评估任务,处理印度 D2C 品牌的活动转录文本和元数据。有一次例行执行的午夜定时任务正在解析两百多个网红视频的指标数据,主 API 客户端突然抛出 429 Too Many Requests 错误,整个索引流水线中途停摆,一半数据库表陷入状态不一致。

先试了什么(以及为什么没用)

一开始,我用的是单个静态环境变量(process.env.GEMINI_API_KEY),直接加载到 Google Gen AI SDK 客户端初始化里。遇到限流错误后,我的第一个尝试很天真——把 API 调用包进简单的 try/catch,再套一个指数退避循环。

import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

async function generateWithRetry(prompt: string, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await ai.models.generateContent({
        model: 'gemini-3.8-flash',
        contents: prompt,
      });
    } catch (error: any) {
      if (error.status === 429 && i < retries - 1) {
        await new Promise((res) => setTimeout(res, Math.pow(2, i) * 1000));
        continue;
      }
      throw error;
    }
  }
}

这上线后还是失败了。原因很简单:如果单次定时任务的总 token 消耗已经超过单个密钥的每分钟请求上限,指数退避只是把不可避免的结果往后拖。令牌桶都空了,等两秒或四秒再拿同一个桶去重试,对解决配额耗尽毫无帮助。定时任务最终还是撞上 Vercel 的执行时长限制而超时。

实际的解决办法

为了在无服务器环境中干净地绕过单个密钥的速率限制,我不再只用一个环境变量,而是实现了一个轮询式的密钥轮换辅助器。它会在存放在 process.env.GEMINI_API_KEYS 中的一组 API 密钥之间循环切换,这些密钥用逗号分隔。每次发出生成请求时,辅助器会用原子计数器或取模运算选出下一个可用密钥的索引;如果某个密钥明确遭到配额拒绝,也能优雅地回退。

import { GoogleGenAI } from '@google/genai';

class GeminiKeyPool {
  private keys: string[];
  private currentIndex = 0;

  constructor() {
    const rawKeys = process.env.GEMINI_API_KEYS || '';
    this.keys = rawKeys.split(',').map((k) => k.trim()).filter(Boolean);
    if (this.keys.length === 0) {
      throw new Error('No Gemini API keys found in pool configuration.');
    }
  }

  public getClient(): GoogleGenAI {
    const apiKey = this.keys[this.currentIndex];
    // Rotate index for the next call
    this.currentIndex = (this.currentIndex + 1) % this.keys.length;
    return new GoogleGenAI({ apiKey });
  }

  public async executeWithRotation<T>(
    operation: (client: GoogleGenAI) => Promise<T>
  ): Promise<T> {
    let attempts = 0;
    const maxAttempts = this.keys.length;

    while (attempts < maxAttempts) {
      const client = this.getClient();
      try {
        return await operation(client);
      } catch (error: any) {
        // Check if error is rate limit (429) or quota exhausted
        if (error?.status === 429 || error?.message?.includes('RESOURCE_EXHAUSTED')) {
          attempts++;
          console.warn(`Gemini API key exhausted. Rotating pool (Attempt ${attempts}/${maxAttempts})...`);
          continue;
        }
        throw error;
      }
    }
    throw new Error('All Gemini API keys in the rotation pool have been exhausted.');
  }
}

export const geminiPool = new GeminiKeyPool();

接下来,我们在处理创作者转录文本的后台工作任务里调用这个池封装:

export async function analyzeCreatorTranscript(transcriptText: string) {
  return await geminiPool.executeWithRotation(async (client) => {
    const response = await client.models.
generateContent ({ model : ' gemini-3.8-flash ' , contents : `Analyze this influencer campaign transcript for engagement metrics and sentiment: ${ transcriptText } ` , }); return response . text ; }); }

让我意外的地方

真正没料到的是,Vercel 在多次调用之间会复用无服务器容器(serverless container),这意味着内存里的 currentIndex 会在热启动(warm start)之间保留下来。这反而成了优势——不必在每次新的 cron 执行时反复用数组里的第一个密钥,轮询指针会从上一次热调用停下的位置继续,自然而然地把请求均衡分摊到整个密钥数组上,根本不需要 Redis 或 Neon 这类外部状态存储。

结果

把 AI 处理任务分散到轮转的密钥池之后,每晚 cron 任务要处理 250 多条创作者视频记录,完成情况从时不时因 429 错误失败,变成了稳定 100% 成功。平均每批执行时间稳定在约 14.2 秒,稳稳处在 25 秒安全超时限制之内,彻底不用再手动重启任务了。

接下来怎么走

如果你也在 Next.js 里用 LLM 扩展后台数据处理管线,记得让环境配置能安全地处理多密钥数组。想深入了解我们如何组织数据库查询、以及这些后台工作任务如何配合,可以看看 PulseROI 上的架构说明。

问题背景

在 Vercel 上通过定时后台任务自动分析 YouTube 创作者数据时,一次高并发批处理跑下来,不到两分钟就能把免费层 AI API 额度耗光。PulseROI 用 Gemini 3.8 Flash 持续执行后台评估任务,处理印度 D2C 品牌的营销活动转录文本和元数据。某次例行的午夜 cron 执行中,正在解析两百多个网红视频的指标,主 API 客户端突然抛出错误。

查看原文