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

我在这个问题的两边都经历过。我曾因为构建过程配置不当而无意中对 CMS API 进行了 DDoS 攻击,我也曾实现过速率限制来保护我们自己的服务器免受失控客户端的侵害。这两种经历都教会了我文档中没有涵盖的东西。让我们一起走过所有这些。

目录

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

HTTP 429 究竟是什么意思?

HTTP 429 在 RFC 6585 中定义,发布于 2012 年。规范出奇地简短。要点是:用户(或客户端)在给定时间内发送了过多请求。

就是这样。这是一个速率限制响应。服务器在说,"我理解你的请求,它可能是有效的,但你需要放慢速度。"

这与 403 Forbidden(您无权限)或 503 Service Unavailable(整个服务器在困难中)不同。429 是有针对性的。它是关于您的特定请求速率。

响应应该包括一个 Retry-After 头部,告诉客户端在重试前需要等待多长时间。我说"应该"是因为很多 API 都不费力这样做,这让每个人的日子都不好过。

您会在哪里看到 429 错误

  • 第三方 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 处理程序、后台作业,甚至客户端获取调用中看到过这个模式。

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

您的测试环境、生产环境、本地开发设置和 CI/CD 管道都在使用相同的 API 密钥。每一个看起来都很好,但总体上它们正在大量消耗您的速率限制预算。

5. 没有去抖动的客户端获取

一个按键搜索功能,在每次按键时触发 API 调用。一个每 500 毫秒轮询一次的仪表板。一个无限滚动,触发的获取速度比用户能滚动的速度更快。这些模式绝对可以触发 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 日期。

以下是大多数教程不会告诉您的:许多 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); // 请求之间 200 毫秒
      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 个请求,足够小型项目使用。截至 2026 年初,他们的 Pro 计划从每月 $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 错误的检查清单:

当您接收 429 时

  1. 检查哪个 API 返回 429 — 查看响应 URL,而不仅仅是状态码
  2. 记录 Retry-After 头部 — 如果它一直很长,您可能需要更高级的计划
  3. 审核您的请求模式 — 您在进行重复调用吗?您能批处理请求吗?
  4. 实现缓存 — 使用 stale-while-revalidate、Redis 缓存或 Next.js ISR 来减少 API 调用
  5. 检查多个环境是否共享 API 密钥 — 这是最常见的"神秘" 429 原因

当您发送 429 时

  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/ratelimit 包的 Upstash Redis。您可以在个别路由级别或使用 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 的常见起点是每分钟 60 个请求每个 IP,或每小时 1,000 个请求每个 API 密钥。对于认证用户,您可能允许更多。关键是监控实际使用模式,设置限制以适应合法使用和一些余地,并根据真实数据调整。从更严格开始并放松 — 比在用户依赖更高费率后收紧限制要容易。

为什么我在静态网站构建期间收到 429 错误? 像 Next.js 和 Astro 这样的静态网站生成器在构建时为每个页面获取数据。如果您有数百或数千个页面,那就是数百或数千次 API 调用迅速连续进行。大多数 CMS API 的速率限制在每秒 5-20 个请求之间。使用 p-limit 或类似库将并发上限为 3-5 个同时请求,在批次之间添加小延迟,并考虑使用增量构建(Next.js 中的 ISR,或 Astro 的增量内容集合)来避免一次性重建所有内容。