HTTP 429 Too Many Requests:開發者忽略的速率限制
你的部署在週五下午4點發布。監控面板在三個儀表板上閃爍紅色。使用者在結帳、登入以及每個重要的API呼叫上都看到429錯誤。你的速率限制器在拒絕請求——或更糟的是,第三方API在拒絕你,而你的重試邏輯讓情況變得更糟。HTTP 429 Too Many Requests狀態碼剛好成為你和工作正常週末之間的唯一障礙。大多數開發者知道429意味著「放慢速度」。他們忽略的是哪些請求應該重試、何時應該指數級退避,以及如何設定真正能防止死亡螺旋的Retry-After標頭。以下是你的API開始拒絕流量時真正發生的情況。
我在這個問題的兩側都經歷過。我曾是那個因為錯誤配置的建置流程而意外DDoS攻擊CMS API的開發者,我也曾是實現速率限制以保護我們自己伺服器免受失控客戶端的人。這兩種經驗都教了我一些文件中未涵蓋的內容。讓我們一起詳細瞭解所有內容。
目錄
- HTTP 429實際上意味著什麼?
- 429錯誤的常見原因
- Retry-After標頭說明
- 如何作為客戶端處理429錯誤
- 在Next.js API路由中實現速率限制
- 速率限制策略比較
- Astro和其他框架中的速率限制
- 在生產環境中監控和除錯429錯誤
- 常見問題

HTTP 429實際上意味著什麼?
HTTP 429在RFC 6585中定義,發佈於2012年。該規範出乎意料地簡短。要點是:使用者(或客戶端)在給定的時間內發送了太多請求。
就是這樣。這是一個速率限制回應。伺服器在說,「我理解你的請求,它可能是有效的,但你需要放慢速度。」
這與403 Forbidden(你不被允許)或503 Service Unavailable(整個伺服器在苦苦掙扎)不同。429是針對性的。它關於你的請求速率。
回應應該包含一個Retry-After標頭,告訴客戶端再試一次前要等多久。我說「應該」是因為許多API根本不費力,這讓每個人的生活變得更加困難。
你會在哪裡看到野生429s
- 第三方API:Stripe、OpenAI、GitHub、Contentful、Sanity——它們都有速率限制
- CDN和託管平台:Vercel、Cloudflare和AWS將在你擊中邊緣速率限制時返回429s
- 你自己的API:如果你已實現速率限制(你應該有的)
- 建置流程:對CMS API執行每個頁面的靜態站點生成可以輕易觸發速率限制
- 網路抓取:如果你積極從外部來源獲取資料
429錯誤的常見原因
讓我分解我在生產中實際遇到的場景,大致按出現頻率排列。
1. 靜態站點建置猛力敲擊無頭CMS
這是與無頭架構一起工作的團隊最常遇到的問題。你有一個有2000頁的站點,每個頁面都需要來自CMS的資料。你的建置流程並行發出所有這些請求,CMS看到大規模尖峰,開始返回429s。你的建置失敗。
我們在進行無頭CMS項目時定期看到這個。修復涉及請求佇列和並行限制,我將在下面介紹。
2. 遺漏或破損的快取
如果每個頁面載入由於快取層不工作而觸發新的API呼叫,你會快速擊中速率限制——特別是在流量尖峰期間。我曾除錯過一個Next.js應用,其中revalidate被意外設定為0,意味著ISR實際上被禁用。每個訪客觸發對Contentful的新API呼叫。花費約45分鐘的真實流量才開始獲得429s。
3. 沒有退避的重試迴圈
你的程式碼獲得錯誤,立即重試,獲得另一個錯誤,立即重試...恭喜,你建立了一個速率限制觸發機器。我在webhook處理器、背景工作和甚至客戶端fetch呼叫中看到過這種模式。
4. 多個服務共享一個API金鑰
你的預備環境、你的生產環境、你的本地開發設定和你的CI/CD管道都使用同一個API金鑰。每一個看起來都很好,但總體上它們在燃燒你的速率限制預算。
5. 沒有防抖的客戶端擷取
搜尋輸入時每次按鍵都觸發API呼叫的功能。每500ms輪詢一次的儀表板。比使用者滾動速度更快觸發擷取的無限捲動。這些模式絕對可以觸發429s,特別是在乘以所有使用者時。
6. 實際濫用或攻擊
有時429正在做它應該做的事——保護你的伺服器免受某人發送不合理的請求數量。機器人、認證填充、抓取——速率限制是你的第一道防線。
Retry-After標頭說明
Retry-After標頭是伺服器告訴你確切何時再試一次的方式。它可以以兩種格式出現:
等待的秒數:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
具體日期/時間:
HTTP/1.1 429 Too Many Requests
Retry-After: Thu, 01 Jan 2026 00:00:00 GMT
秒格式遠更常見。日期格式使用RFC 7231中定義的HTTP日期。
以下是大多數教程不會告訴你的:許多API根本不發送Retry-After,或他們不一致地發送它。OpenAI的API通常包含它。GitHub的API連同X-RateLimit-Reset包含它。許多較小的API只是發送一個裸體429,讓你猜測。
一些API也發送額外的速率限制標頭:
| 標頭 | 目的 | 範例 |
|--------|---------|--------||
| X-RateLimit-Limit | 每個窗口允許的最大請求 | 100 |
| X-RateLimit-Remaining | 當前窗口中剩餘的請求 | 0 |
| X-RateLimit-Reset | Unix時間戳,當窗口重置時 | 1735689600 |
| Retry-After | 重試前等待的秒數 | 30 |
始終檢查這些標頭。它們讓你實現更智慧的重試邏輯,甚至在擊中限制前主動放慢速度。

如何作為客戶端處理429錯誤
當你是接收429錯誤的人時,以下是如何正確處理它們。
帶有抖動的指數退避
這是黃金標準。不要只是等待固定的時間——隨著每次重試增加延遲指數倍,並添加一些隨機性(抖動)以防止雷鳴羊群問題。
async function fetchWithRetry(
url: string,
options: RequestInit = {},
maxRetries: number = 5
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
if (attempt === maxRetries) {
throw new Error(`Still getting 429 after ${maxRetries} retries`);
}
// 首先檢查Retry-After標頭
const retryAfter = response.headers.get('Retry-After');
let delay: number;
if (retryAfter) {
// 可能是秒或日期
const parsed = parseInt(retryAfter, 10);
if (!isNaN(parsed)) {
delay = parsed * 1000;
} else {
delay = new Date(retryAfter).getTime() - Date.now();
}
} else {
// 帶有抖動的指數退避
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
delay = baseDelay + jitter;
}
console.log(`Rate limited. Waiting ${Math.round(delay / 1000)}s before retry ${attempt + 1}`);
await new Promise(resolve => setTimeout(resolve, delay));
}
// TypeScript要求這個,雖然我們永遠不會到達它
throw new Error('Unexpected end of retry loop');
}
建置流程的請求佇列
對於靜態站點生成,其中你需要進行數百或數千個API呼叫,使用具有並行控制的佇列:
import pLimit from 'p-limit';
// 限制為5個並行請求
const limit = pLimit(5);
const pages = await getAllPageSlugs(); // 返回 ['/', '/about', '/blog/post-1', ...]
const results = await Promise.all(
pages.map(slug =>
limit(() => fetchWithRetry(`https://api.cms.com/pages/${slug}`))
)
);
p-limit函式庫(2026年每週250多萬npm下載)是我的首選。你也可以在請求之間添加延遲:
const limit = pLimit(3);
const delay = (ms: number) => new Promise(r => setTimeout(r, ms));
const results = await Promise.all(
pages.map((slug, i) =>
limit(async () => {
if (i > 0) await delay(200); // 請求之間200ms
return fetchWithRetry(`https://api.cms.com/pages/${slug}`);
})
)
);
在Next.js API路由中實現速率限制
現在讓我們翻轉另一面——你在建立API,需要保護它。如果你用Next.js建立,以下是如何添加速率限制到你的API路由。
簡單的記憶體內速率限制器
對於單伺服器部署或在開發過程中,這有效:
// lib/rate-limit.ts
type RateLimitEntry = {
count: number;
resetTime: number;
};
const rateLimitMap = new Map<string, RateLimitEntry>();
export function rateLimit({
windowMs = 60 * 1000,
maxRequests = 100,
}: {
windowMs?: number;
maxRequests?: number;
} = {}) {
return function check(identifier: string): {
allowed: boolean;
remaining: number;
resetIn: number;
} {
const now = Date.now();
const entry = rateLimitMap.get(identifier);
if (!entry || now > entry.resetTime) {
rateLimitMap.set(identifier, {
count: 1,
resetTime: now + windowMs,
});
return { allowed: true, remaining: maxRequests - 1, resetIn: windowMs };
}
if (entry.count >= maxRequests) {
return {
allowed: false,
remaining: 0,
resetIn: entry.resetTime - now,
};
}
entry.count++;
return {
allowed: true,
remaining: maxRequests - entry.count,
resetIn: entry.resetTime - now,
};
};
}
在Next.js App Router API路由中使用它:
// app/api/data/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rateLimit } from '@/lib/rate-limit';
const limiter = rateLimit({ windowMs: 60_000, maxRequests: 30 });
export async function GET(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
const { allowed, remaining, resetIn } = limiter(ip);
if (!allowed) {
return NextResponse.json(
{ error: 'Too many requests. Please slow down.' },
{
status: 429,
headers: {
'Retry-After': String(Math.ceil(resetIn / 1000)),
'X-RateLimit-Limit': '30',
'X-RateLimit-Remaining': '0',
},
}
);
}
// 你的實際路由邏輯在這裡
return NextResponse.json(
{ data: 'Here you go' },
{
headers: {
'X-RateLimit-Limit': '30',
'X-RateLimit-Remaining': String(remaining),
},
}
);
}
使用Upstash Redis的生產速率限制
當你在Vercel等無伺服器平台上執行時,記憶體內方法會崩解,因為每個函數呼叫可能會擊中不同的實例。你需要一個共享存儲。Upstash Redis是2026年最受歡迎的選擇。
npm install @upstash/ratelimit @upstash/redis
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
export const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(30, '60 s'),
analytics: true,
prefix: 'api-ratelimit',
});
// app/api/data/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';
export async function GET(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
const retryAfter = Math.ceil((reset - Date.now()) / 1000);
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{
status: 429,
headers: {
'Retry-After': String(retryAfter),
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': String(reset),
},
}
);
}
return NextResponse.json({ data: 'Success' }, {
headers: {
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': String(remaining),
},
});
}
Upstash的免費層給你每天10,000個請求,這對小項目來說足夠。他們的Pro計畫從2026年初的$10/月開始,每天500K命令。
中間件級速率限制
如果你想在所有API路由上進行速率限制,Next.js中間件是地方:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
const { success, reset } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
},
}
);
}
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};
速率限制策略比較
並非所有速率限制演算法都相等。以下是主要演算法的比較方式:
| 演算法 | 它如何工作 | 優點 | 缺點 | 最適合 |
|---|---|---|---|---|
| 固定窗口 | 計數固定時間窗口中的請求(例如,每分鐘) | 實現簡單 | 窗口邊界處的突發可以允許2倍限制 | 簡單API、內部工具 |
| 滑動窗口 | 計數滾動時間段上的請求 | 更平滑的分佈 | 略微複雜、更多記憶體 | 大多數生產API |
| 代幣桶 | 代幣以穩定速率補充,每個請求花費一個代幣 | 允許控制的突發 | 更複雜的狀態管理 | 需要突發容差的API |
| 洩漏桶 | 請求進入佇列並以固定速率處理 | 非常平滑的輸出速率 | 可能添加延遲、請求可能被丟棄 | webhook傳遞、工作處理 |
| 滑動窗口日誌 | 存儲每個請求的時間戳 | 最精確 | 規模上的高記憶體使用 | 低容量、高精度需求 |
對於大多數網路應用,滑動窗口是最佳點。這是Upstash預設使用的,除非你有特定的理由選擇其他,否則我會推薦它。
Astro和其他框架中的速率限制
如果你用Astro建立,速率限制的工作方式不同,因為Astro主要是靜態優先框架。但在Astro的伺服器端點(在SSR模式下可用)中,概念是相同的:
// src/pages/api/data.ts
import type { APIRoute } from 'astro';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(30, '60 s'),
});
export const GET: APIRoute = async ({ request }) => {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
const { success, reset } = await ratelimit.limit(ip);
if (!success) {
return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
},
});
}
return new Response(JSON.stringify({ data: 'Hello' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
對於在Cloudflare Workers上邊緣部署的應用,你也可能考慮Cloudflare的內置速率限制規則,它在基礎設施級別運行,可以處理遠超過應用級別解決方案的流量。他們的高級速率限制在Business計畫上從每10,000個好請求$0.05開始。
在生產環境中監控和除錯429錯誤
你無法修復看不到的東西。以下是處理生產環境中429錯誤的我的檢查清單:
當你接收429s時
- 檢查哪個API返回429——查看回應URL,不只是狀態碼
- 記錄
Retry-After標頭——如果它一貫非常長,你可能需要更高的層級計畫 - 審計你的請求模式——你在進行冗餘呼叫嗎?你可以批量請求嗎?
- 實現快取——使用
stale-while-revalidate、Redis快取或Next.js ISR來減少API呼叫 - 檢查多個環境是否共享API金鑰——這是最常見的「神祕」429原因
當你發送429s時
- 設定儀表板——跟蹤429回應速率
- 識別頂級違反者——哪些IP位址或API金鑰最常擊中限制?
- 審查你的限制——它們太嚴格了嗎?太寬鬆了?檢查你的伺服器容量並調整
- 始終發送
Retry-After——是一個好的API公民 - 包含有幫助的錯誤訊息——告訴客戶端哪個限制他們擊中,何時再試
精心製作的429回應主體看起來像這樣:
{
"error": {
"type": "rate_limit_exceeded",
"message": "You've exceeded 30 requests per minute. Please wait before retrying.",
"retryAfter": 42,
"documentation": "https://docs.yourapi.com/rate-limits"
}
}
這無限好於只是{ "error": "Too many requests" }。
如果你在無頭架構上處理持續的速率限制問題——無論是在建置期間、執行時或兩者——可能值得聯絡我們來討論你的架構。我們在不同CMS和框架組合中看到許多這些問題,通常有一個模式級修復,而不是只是繃帶症狀。
常見問題
HTTP 429 Too Many Requests意味著什麼?
HTTP 429是一個狀態碼,意味著你在給定的時間段內向伺服器發送了太多請求。伺服器在速率限制你——它要求你放慢速度。這不是身份驗證錯誤或伺服器錯誤;你的請求可能是有效的,只是有太多。伺服器應該包含告訴你何時再試的Retry-After標頭。
我如何修復429錯誤?
如果你從API收到429錯誤,在你的重試邏輯中實現帶有抖動的指數退避,減少你的請求頻率,添加快取以避免冗餘呼叫,並尊重Retry-After標頭。如果你在建置期間擊中限制,使用具有並行限制的請求佇列。如果它持續發生,你可能需要升級到具有更寬鬆速率限制的更高API計畫。
什麼是Retry-After標頭?
Retry-After標頭與429(或503)回應一起發送,告訴客戶端在進行另一個請求前要等多久。它可以指定為秒數(例如,Retry-After: 60)或HTTP日期(例如,Retry-After: Thu, 01 Jan 2026 00:00:00 GMT)。並非所有API都包含此標頭,但設計良好的API會。
我如何在Next.js中實現速率限制?
對於開發或單伺服器部署,你可以使用記憶體內Map來跟蹤每個IP位址的請求計數。對於像Vercel這樣的平台上的生產無伺服器部署,使用Upstash Redis與@upstash/ratelimit套件。你可以在單個路由級別或使用Next.js中間件在所有API路由上應用速率限制。
429和503錯誤之間有什麼區別?
429 Too Many Requests特別是關於速率限制——你的客戶端發送太多請求。503 Service Unavailable意味著伺服器超載或正在維護,無法處理任何人的任何請求。兩者都可以包含Retry-After標頭,但它們表示非常不同的問題。429針對你;503影響每個人。
速率限制可以防止DDoS攻擊嗎? 速率限制是防禦DDoS攻擊的一個層面,但單獨不足以。應用級速率限制(就像你在Next.js中實現的)可以處理中等濫用,但嚴重的DDoS攻擊需要在基礎設施級別緩解——使用Cloudflare、AWS Shield或你的託管提供商的內置保護等服務。將應用級速率限制視為保鏢,將基礎設施級別保護視為堡壘牆。
我應該為我的API設定什麼速率限制? 這完全取決於你的使用情況。公開API的常見起點是每個IP每分鐘60個請求,或每個API金鑰每小時1,000個請求。對於已驗證使用者,你可能允許更多。關鍵是監控實際使用模式,設定適應合法使用並有一些迴旋空間的限制,並基於真實資料調整。從更限制性開始並放鬆——比在使用者依賴更高速率後收緊限制容易。
為什麼我在靜態站點建置期間收到429錯誤?
像Next.js和Astro這樣的靜態站點生成器在建置時為每個頁面獲取資料。如果你有數百或數千頁,那是數百或數千個API呼叫快速連續。大多數CMS API的速率限制在每秒5-20個請求之間。使用p-limit或類似的函式庫來限制並行於3-5個同時請求,在批次之間添加小延遲,並考慮使用增量建置(Next.js中的ISR或Astro的增量內容收集)來避免一次重建所有內容。