在Next.js客户端应用中处理Gemini API的429请求过多错误
问题:Gemini 2.5 Pro免费层配额耗尽触发429错误,重试机制无效
在Next.js客户端应用中使用@google/genai包生成面试题时,反复出现429 Too Many Requests错误。已经实现了指数退避重试机制(5秒、10秒、20秒),但三次尝试后依然失败。错误信息显示已超出Gemini 2.5 Pro免费层的所有配额限制,包括每日令牌数、每日请求数、每分钟请求数和每分钟令牌数。
相关代码
GeminiAIModal.js
import { GoogleGenAI } from '@google/genai'; const apiKey = process.env.NEXT_PUBLIC_GEMINI_API_KEY; if (!apiKey) { throw new Error('NEXT_PUBLIC_GEMINI_API_KEY is not set in environment variables'); } const ai = new GoogleGenAI({ apiKey }); const config = { thinkingConfig: { thinkingBudget: -1, // Unlimited thinking budget as per your UI }, responseMimeType: 'application/json', // Structured output }; const model = 'gemini-2.5-pro'; async function withRetry(fn, maxRetries = 3, baseDelay = 5000) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { if (error.message.includes('429') && attempt < maxRetries) { const delay = baseDelay * Math.pow(2, attempt - 1); console.log(`429 error, retrying after ${delay}ms (attempt ${attempt}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); continue; } throw error; } } } export async function generateInterviewQuestions(prompt) { try { const result = await withRetry(async () => { const response = await ai.models.generateContentStream({ model, config, contents: [ { role: 'user', parts: [{ text: prompt }], }, ], }); let fullText = ''; for await (const chunk of response) { fullText += chunk.text; } return fullText; }); return JSON.parse(result); } catch (error) { console.error('Error in generateInterviewQuestions:', error); throw new Error('Failed to generate interview questions from Gemini API'); } }
AddCardWithDialog.jsx
'use client'; import React, { useState } from 'react'; import { Dialog } from '@headlessui/react'; import { XMarkIcon } from '@heroicons/react/24/solid'; import { generateInterviewQuestions } from '../../utils/GeminiAIModal'; function AddCardWithDialog({ title, description }) { const [isOpen, setIsOpen] = useState(false); const [form, setForm] = useState({ role: '', description: '', experience: '', }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const handleStartInterview = async (e) => { e.preventDefault(); setIsLoading(true); setError(null); const contents = `job position: ${form.role}, job description: ${form.description}, no of year experience: ${form.experience}, depends of those info give me 5 interview qs in json format give qs and answers in json format`; try { const result = await generateInterviewQuestions(contents); console.log('Interview Questions:', result); setIsOpen(false); } catch (err) { console.error('Error generating questions:', err); setError('Failed to generate interview questions. Please try again.'); } finally { setIsLoading(false); } }; const handleChange = (e) => { setForm({ ...form, [e.target.name]: e.target.value }); }; return ( <> <div onClick={() => setIsOpen(true)} className='p-10 border rounded-2xl bg-secondary hover:scale-105 hover:shadow-lg cursor-pointer transition-all' > <h2 className='text-lg text-center font-semibold text-gray-800'>{title}</h2> </div> <Dialog open={isOpen} onClose={() => setIsOpen(false)} className='relative z-50'> <div className='fixed inset-0 bg-black/30 backdrop-blur-sm' /> <div className='fixed inset-0 flex items-center justify-center p-4'> <Dialog.Panel className='relative w-full max-w-lg rounded-2xl bg-white p-6 shadow-xl'> <button onClick={() => setIsOpen(false)} className='absolute top-4 right-4 text-gray-500 hover:text-red-600 transition-colors' > <XMarkIcon className='w-6 h-6' /> </button> <Dialog.Title className='text-2xl font-bold text-gray-900 mb-1'> Tell us more about Job you are interviewing </Dialog.Title> <Dialog.Description className='text-gray-500 mb-6'> Add Details about job position, your skills and year of experience </Dialog.Description> <div className='space-y-4'> <div> <label className='block text-sm font-medium mb-1 text-gray-700'> Job Position / Role name </label> <input type='text' name='role' value={form.role} onChange={handleChange} className='w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500' /> </div> <div> <label className='block text-sm font-medium mb-1 text-gray-700'> Job Description / Tech Stack in Short </label> <textarea name='description' rows={3} value={form.description} onChange={handleChange} className='w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none' /> </div> <div> <label className='block text-sm font-medium mb-1 text-gray-700'> No of Year Experience </label> <input type='number' name='experience' value={form.experience} onChange={handleChange} className='w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500' /> </div> </div> {error && ( <p className='mt-4 text-red-600 text-sm'>{error}</p> )} <div className='mt-6 flex justify-end space-x-3'> <button onClick={() => setIsOpen(false)} className='px-4 py-2 rounded-md bg-gray-200 text-gray-600 hover:bg-gray-300' disabled={isLoading} > Cancel </button> <button onClick={handleStartInterview} className='px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-400' disabled={isLoading} > {isLoading ? 'Generating...' : 'Start Interview'} </button> </div> </Dialog.Panel> </div> </Dialog> </> ); } export default AddCardWithDialog;
错误日志
<project-path>\utils\GeminiAIModal.js:24 429 error, retrying after 5000ms (attempt 1/3) <project-path>\utils\GeminiAIModal.js:36 POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse 429 (Too Many Requests) [...stack trace...] <project-path>\utils\GeminiAIModal.js:24 429 error, retrying after 10000ms (attempt 2/3) [...similar 429 error...] <project-path>\utils\GeminiAIModal.js:54 Error in generateInterviewQuestions: ClientError: got status: 429 . {"error":{"message":"{\n \"error\": {\n \"code\": 429,\n \"message\": \"You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits.\",\n \"status\": \"RESOURCE_EXHAUSTED\",\n \"details\": [\n {\n \"@type\": \"type.googleapis.com/google.rpc.QuotaFailure\",\n \"violations\": [\n {\n \"quotaMetric\": \"generativelanguage.googleapis.com/generate_content_free_tier_input_token_count\",\n \"quotaId\": \"GenerateContentInputTokensPerModelPerDay-FreeTier\",\n \"quotaDimensions\": {\n \"location\": \"global\",\n \"model\": \"gemini-2.5-pro\"\n }\n },\n {\n \"quotaMetric\": \"generativelanguage.googleapis.com/generate_content_free_tier_requests\",\n \"quotaId\": \"GenerateRequestsPerDayPerProjectPerModel-FreeTier\",\n \"quotaDimensions\": {\n \"model\": \"gemini-2.5-pro\",\n \"location\": \"global\"\n }\n },\n {\n \"quotaMetric\": \"generativelanguage.googleapis.com/generate_content_free_tier_requests\",\n \"quotaId\": \"GenerateRequestsPerMinutePerProjectPerModel-FreeTier\",\n \"quotaDimensions\": {\n \"model\": \"gemini-2.5-pro\",\n \"location\": \"global\"\n }\n },\n {\n \"quotaMetric\": \"generativelanguage.googleapis.com/generate_content_free_tier_input_token_count\",\n \"quotaId\": \"GenerateContentInputTokensPerModelPerMinute-FreeTier\",\n \"quotaDimensions\": {\n \"location\": \"global\",\n \"model\": \"gemini-2.5-pro\"\n }\n }\n ]\n },\n {\n \"@type\": \"type.googleapis.com/google.rpc.Help\",\n \"links\": [\n {\n \"description\": \"Learn more about Gemini API quotas\",\n \"url\": \"https://ai.google.dev/gemini-api/docs/rate-limits\"\n }\n ]\n },\n {\n \"@type\": \"type.googleapis.com/google.rpc.RetryInfo\",\n \"retryDelay\": \"49s\"\n }\n ]\n }\n}\n","code":429,"status":""}} at throwErrorIfNotOK (<project-path>\node_modules\@google\genai\dist\web\index.mjs:11201:1) [...stack trace...] <project-path>\components\ui\AddCardWithDialog.jsx:34 Error generating questions: Error: Failed to generate interview questions from Gemini API at generateInterviewQuestions (<project-path>\utils\GeminiAIModal.js:55:15) [...stack trace...]
解决方案
1. 处理配额耗尽问题
- 免费层配额每日重置,测试场景可等次日再使用;正式使用直接升级付费套餐,提升配额上限。
2. 优化重试逻辑,遵循API返回的重试延迟
当前固定指数退避的间隔不符合API要求,应优先使用错误返回的retryDelay值,避免在配额未恢复时反复请求。修改withRetry函数:
async function withRetry(fn, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { if (error.message.includes('429') && attempt < maxRetries) { let delay = 5000 * Math.pow(2, attempt - 1); // 解析错误中的官方重试延迟 if (error.response?.data?.error?.details) { const retryInfo = error.response.data.error.details.find(item => item['@type'] === 'type.googleapis.com/google.rpc.RetryInfo'); if (retryInfo?.retryDelay) { const seconds = parseInt(retryInfo.retryDelay.replace('s', ''), 10); delay = seconds * 1000; } } console.log(`429 error, retrying after ${delay}ms (attempt ${attempt}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); continue; } throw error; } } }
3. 减少请求和令牌消耗
- 客户端限流:给
handleStartInterview加防抖逻辑,限制用户1分钟内只能发起一次请求,避免重复点击超额消耗配额。 - 精简提示词:简化提示内容,减少输入令牌数,示例:
const contents = `根据以下信息生成5道面试题及答案,返回JSON格式: 职位:${form.role} 工作描述:${form.description} 经验年限:${form.experience}年`;
- 迁移到服务端调用:把Gemini API调用移到Next.js的API路由中,避免在客户端暴露API密钥(当前
NEXT_PUBLIC_GEMINI_API_KEY会导致密钥前端可见,存在被盗用风险),同时在服务端基于IP或用户会话做更严格的请求限制。
4. 切换到低配额限制的模型
暂时不想付费的话,可换成Gemini 1.5 Flash免费层,它的配额更宽松,适合测试场景,只需修改模型名称:
const model = 'gemini-1.5-flash';
内容的提问来源于stack exchange,提问作者Mohamed Hedi ben salah
相关产品推荐
相关产品推荐

