你的部署在週五下午4點發布。監控面板在三個儀表板上閃爍紅色。使用者在結帳、登入以及每個重要的API呼叫上都看到429錯誤。你的速率限制器在拒絕請求——或更糟的是,第三方API在拒絕,而你的重試邏輯讓情況變得更糟。HTTP 429 Too Many Requests狀態碼剛好成為你和工作正常週末之間的唯一障礙。大多數開發者知道429意味著「放慢速度」。他們忽略的是哪些請求應該重試、何時應該指數級退避,以及如何設定真正能防止死亡螺旋的Retry-After標頭。以下是你的API開始拒絕流量時真正發生的情況。

我在這個問題的兩側都經歷過。我曾是那個因為錯誤配置的建置流程而意外DDoS攻擊CMS API的開發者,我也曾是實現速率限制以保護我們自己伺服器免受失控客戶端的人。這兩種經驗都教了我一些文件中未涵蓋的內容。讓我們一起詳細瞭解所有內容。

目錄

HTTP 429 Too Many Requests:原因、修復和速率限制

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 |

始終檢查這些標頭。它們讓你實現更智慧的重試邏輯,甚至在擊中限制前主動放慢速度。

HTTP 429 Too Many Requests:原因、修復和速率限制——架構

如何作為客戶端處理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時

  1. 檢查哪個API返回429——查看回應URL,不只是狀態碼
  2. 記錄Retry-After標頭——如果它一貫非常長,你可能需要更高的層級計畫
  3. 審計你的請求模式——你在進行冗餘呼叫嗎?你可以批量請求嗎?
  4. 實現快取——使用stale-while-revalidate、Redis快取或Next.js ISR來減少API呼叫
  5. 檢查多個環境是否共享API金鑰——這是最常見的「神祕」429原因

當你發送429s時

  1. 設定儀表板——跟蹤429回應速率
  2. 識別頂級違反者——哪些IP位址或API金鑰最常擊中限制?
  3. 審查你的限制——它們太嚴格了嗎?太寬鬆了?檢查你的伺服器容量並調整
  4. 始終發送Retry-After——是一個好的API公民
  5. 包含有幫助的錯誤訊息——告訴客戶端哪個限制他們擊中,何時再試

精心製作的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的增量內容收集)來避免一次重建所有內容。