你在周五下午部署代码(我知道,我知道),一切看起来都很顺利,然后监控系统就像圣诞树一样闪闪发光。用户收到 429 错误。你的 API 拒绝了请求。或者反过来——你在调用第三方 API,而他们在拒绝你。无论如何,HTTP 429 Too Many Requests 状态码刚刚成为了你这一天最重要的事情。

我经历过两种情况。我曾经因为构建流程配置错误而意外地 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 在你击中它们边缘速率限制时会返回 429
  • 你自己的 API:如果你已实现速率限制(你应该有)
  • 构建流程:静态网站生成为每个页面击中 CMS API 可以轻易触发速率限制
  • 网络爬取:如果你积极地从外部源获取数据

429 错误的常见原因

让我分解一下我在生产环境中实际遇到的场景,大致按出现频率排列。

1. 静态网站构建猛轰无头 CMS

这对使用无头架构的团队来说是最常见的问题。你有一个有 2,000 个页面的网站,每个页面都需要来自你的 CMS 的数据。你的构建流程并行地触发所有这些请求,CMS 看到一个巨大的峰值,开始返回 429。你的构建失败。

在进行无头 CMS 项目时,我们定期看到这种情况。修复涉及请求队列和并发限制,我将在下面介绍。

2. 缺失或损坏的缓存

如果每个页面加载都触发一个新的 API 调用,因为你的缓存层不工作,你会很快达到速率限制——特别是在流量峰值时。我曾经调试过一个 Next.js 应用,其中 revalidate 被意外设置为 0,意味着 ISR 实际上被禁用了。每个访问者都触发了一个到 Contentful 的新 API 调用。大约需要 45 分钟的真实流量才开始收到 429。

3. 没有退避的重试循环

你的代码收到一个错误,立即重试,收到另一个错误,立即重试...恭喜,你已经构建了一个触发速率限制的机器。我在 webhook 处理程序、后台任务,甚至客户端 fetch 调用中看到过这个模式。

4. 多个服务共享一个 API 密钥

你的暂存环境、生产环境、本地开发设置和 CI/CD 流程都在使用同一个 API 密钥。每一个单独看起来都没问题,但总体上他们在快速消耗你的速率限制预算。

5. 没有去抖动的客户端 Fetch

一个搜索即输入的功能,在每次按键时都触发 API 调用。一个每 500ms 轮询一次的仪表板。一个无限滚动,触发 fetch 速度比用户滚动速度更快。这些模式绝对可以触发 429,特别是当跨越所有用户乘以时。

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-date。

以下是大多数教程不会告诉你的内容:许多 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 库(2025 年每周 npm 下载量超过 250 万次)是我的首选。你也可以添加请求之间的延迟:

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 Routes 中实现速率限制

现在让我们翻转到另一边——你正在构建一个 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 是 2025 年这方面最受欢迎的选择。

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 个请求,这对于小项目来说足够了。他们的专业计划从 2025 年初的每月 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 的内置速率限制规则,它在基础设施级别运行,可以处理远远超过应用级别解决方案的流量。他们的高级速率限制在商业计划中每 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 的增量内容集合)以避免一次性重建所有内容。