Estás desplegando un viernes por la tarde (ya lo sé, ya lo sé), todo se ve bien, y de repente tu monitoreo se ilumina como un árbol de Navidad. Los usuarios reciben errores 429. Tu API rechaza peticiones. O quizás es al revés — estás llamando a una API de terceros y ellos te están rechazando a ti. De cualquier manera, el código de estado HTTP 429 Too Many Requests acaba de convertirse en lo más importante de tu día.

He estado en ambos lados de esto. He sido el desarrollador que accidentalmente hizo DDoS a una API de CMS por un proceso de compilación mal configurado, y he sido quien implementó rate limiting para proteger nuestros propios servidores de clientes descontrolados. Ambas experiencias me enseñaron cosas que la documentación no cubre. Vamos a recorrer todo esto.

Tabla de Contenidos

HTTP 429 Too Many Requests: Causas, soluciones y rate limiting

¿Qué significa HTTP 429 realmente?

HTTP 429 está definido en RFC 6585, publicado en 2012. La especificación es sorprendentemente corta. Aquí está la esencia: el usuario (o cliente) ha enviado demasiadas peticiones en una cantidad de tiempo determinada.

Eso es todo. Es una respuesta de rate limiting. El servidor está diciendo: "Entendí tu petición, probablemente es válida, pero necesitas ir más lentamente".

Esto es diferente de 403 Forbidden (no estás permitido) o 503 Service Unavailable (todo el servidor está teniendo problemas). 429 es específico. Se trata sobre tu tasa de peticiones específicamente.

La respuesta DEBERÍA incluir un encabezado Retry-After diciendo al cliente cuánto tiempo esperar antes de intentar de nuevo. Dije "debería" porque muchas APIs no se molestan, lo que hace la vida de todos más difícil.

Dónde verás 429s en la naturaleza

  • APIs de terceros: Stripe, OpenAI, GitHub, Contentful, Sanity — todos tienen límites de tasa
  • CDNs y plataformas de hosting: Vercel, Cloudflare, y AWS devolverán 429s si golpeas sus límites de tasa en edge
  • Tus propias APIs: Si has implementado rate limiting (y deberías hacerlo)
  • Procesos de compilación: Generación de sitios estáticos que golpea una API de CMS para cada página puede fácilmente disparar límites de tasa
  • Web scraping: Si estás obteniendo datos de fuentes externas agresivamente

Causas comunes de errores 429

Permíteme desglosar los escenarios que realmente he encontrado en producción, clasificados aproximadamente por qué tan a menudo surgen.

1. Compilaciones de sitios estáticos martillando un CMS headless

Este es el que más muerde a los equipos que trabajan con arquitecturas headless. Tienes un sitio con 2,000 páginas, cada una necesitando datos de tu CMS. Tu proceso de compilación dispara todas esas peticiones en paralelo, el CMS ve un pico masivo, y comienza a devolver 429s. Tu compilación falla.

Vemos esto regularmente al trabajar en proyectos de CMS headless. La solución implica queuing de peticiones y límites de concurrencia, que cubrirá abajo.

2. Caché faltante o roto

Si cada carga de página dispara una llamada API fresca porque tu capa de caché no funciona, golpearás límites de tasa rápido — especialmente con picos de tráfico. Una vez debugué una aplicación Next.js donde revalidate fue accidentalmente establecido a 0, significando que ISR fue efectivamente deshabilitado. Cada visitante dispó una nueva llamada API a Contentful. Tomó cerca de 45 minutos de tráfico real para empezar a obtener 429s.

3. Bucles de reintento sin backoff

Tu código obtiene un error, reintenta inmediatamente, obtiene otro error, reintenta inmediatamente... felicitaciones, has construido una máquina disparadora de límites de tasa. He visto este patrón en manejadores de webhook, trabajos en segundo plano, e incluso llamadas de fetch del lado del cliente.

4. Múltiples servicios compartiendo una clave API

Tu entorno de staging, tu entorno de producción, tu configuración local de dev, y tu pipeline de CI/CD están todos usando la misma clave API. Cada uno se ve bien individualmente, pero colectivamente están quemando tu presupuesto de límite de tasa.

5. Fetch del lado del cliente sin debouncing

Una característica de búsqueda-mientras-escribes que dispara una llamada API en cada pulsación. Un dashboard que realiza polling cada 500ms. Un scroll infinito que dispara fetches más rápido de lo que el usuario puede desplazarse. Estos patrones pueden absolutamente disparar 429s, especialmente cuando se multiplican a través de todos tus usuarios.

6. Abuso o ataque actual

A veces un 429 está haciendo exactamente lo que debería — protegiendo tu servidor de alguien enviando un número irrazonable de peticiones. Bots, credential stuffing, scraping — rate limiting es tu primera línea de defensa.

El encabezado Retry-After explicado

El encabezado Retry-After es la forma del servidor de decirte exactamente cuándo intentar de nuevo. Puede venir en dos formatos:

Segundos para esperar:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

Fecha/hora específica:

HTTP/1.1 429 Too Many Requests
Retry-After: Thu, 01 Jan 2026 00:00:00 GMT

El formato de segundos es mucho más común. El formato de fecha usa HTTP-date como está definido en RFC 7231.

Aquí está lo que la mayoría de tutoriales no te dirán: muchas APIs no envían Retry-After en absoluto, o lo envían inconsistentemente. La API de OpenAI generalmente lo incluye. La API de GitHub lo incluye junto con X-RateLimit-Reset. Muchas APIs más pequeñas solo envían un 429 desnudo y te dejan adivinando.

Algunas APIs también envían encabezados adicionales de límite de tasa:

| Encabezado | Propósito | Ejemplo | |--------|---------|--------|| | X-RateLimit-Limit | Máximo de peticiones permitidas por ventana | 100 | | X-RateLimit-Remaining | Peticiones restantes en ventana actual | 0 | | X-RateLimit-Reset | Timestamp Unix cuando la ventana se reinicia | 1735689600 | | Retry-After | Segundos para esperar antes de reintentar | 30 |

Siempre verifica estos encabezados. Te permiten implementar lógica de reintento más inteligente e incluso ralentizar proactivamente antes de golpear el límite.

HTTP 429 Too Many Requests: Causas, soluciones y rate limiting - arquitectura

Cómo manejar errores 429 como cliente

Cuando eres el que recibe errores 429, aquí está cómo manejarlos apropiadamente.

Backoff exponencial con jitter

Este es el estándar de oro. No simplemente esperes una cantidad fija de tiempo — aumenta el retraso exponencialmente con cada reintento, y añade algo de aleatoriedad (jitter) para prevenir problemas de thundering herd.

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`);
    }

    // Check for Retry-After header first
    const retryAfter = response.headers.get('Retry-After');
    let delay: number;

    if (retryAfter) {
      // Could be seconds or a date
      const parsed = parseInt(retryAfter, 10);
      if (!isNaN(parsed)) {
        delay = parsed * 1000;
      } else {
        delay = new Date(retryAfter).getTime() - Date.now();
      }
    } else {
      // Exponential backoff with jitter
      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 wants this, though we'll never reach it
  throw new Error('Unexpected end of retry loop');
}

Request queuing para procesos de compilación

Para generación de sitios estáticos donde necesitas hacer cientos o miles de llamadas API, usa una cola con control de concurrencia:

import pLimit from 'p-limit';

// Limit to 5 concurrent requests
const limit = pLimit(5);

const pages = await getAllPageSlugs(); // Returns ['/', '/about', '/blog/post-1', ...]

const results = await Promise.all(
  pages.map(slug =>
    limit(() => fetchWithRetry(`https://api.cms.com/pages/${slug}`))
  )
);

La librería p-limit (2.5M+ descargas semanales en npm en 2025) es mi opción favorita para esto. También puedes añadir un retraso entre peticiones:

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 between requests
      return fetchWithRetry(`https://api.cms.com/pages/${slug}`);
    })
  )
);

Implementar rate limiting en Next.js API Routes

Ahora voltemos al otro lado — estás construyendo una API y necesitas protegerla. Si estás construyendo con Next.js, aquí está cómo añadir rate limiting a tus API routes.

Rate limiter simple en memoria

Para un despliegue de servidor único o durante desarrollo, esto funciona:

// 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,
    };
  };
}

Usándolo en una Next.js App Router API route:

// 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',
        },
      }
    );
  }

  // Your actual route logic here
  return NextResponse.json(
    { data: 'Here you go' },
    {
      headers: {
        'X-RateLimit-Limit': '30',
        'X-RateLimit-Remaining': String(remaining),
      },
    }
  );
}

Rate limiting en producción con Upstash Redis

El enfoque en memoria se rompe cuando corres en plataformas serverless como Vercel, porque cada invocación de función podría golpear una instancia diferente. Necesitas un almacén compartido. Upstash Redis es la opción más popular para esto en 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),
    },
  });
}

El nivel gratuito de Upstash te da 10,000 peticiones/día, que es bastante para proyectos pequeños. Su plan Pro comienza en $10/mes para 500K comandos diarios a partir de principios de 2025.

Rate limiting a nivel de middleware

Si quieres rate limiting a través de todas tus rutas de API, el middleware de Next.js es el lugar:

// 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*',
};

Estrategias de rate limiting comparadas

No todos los algoritmos de rate limiting son iguales. Aquí está cómo se comparan los principales:

| Algoritmo | Cómo funciona | Pros | Contras | Mejor para | |-----------|-------------|------|------|----------|| | Fixed Window | Cuenta peticiones en ventanas de tiempo fijas (e.g., por minuto) | Simple de implementar | Burst en límites de ventana puede permitir 2x el límite | APIs simples, herramientas internas | | Sliding Window | Cuenta peticiones en un período de tiempo rodante | Distribución más suave | Ligeramente más complejo, más memoria | La mayoría de APIs en producción | | Token Bucket | Los tokens se rellenan a un ritmo constante, cada petición cuesta un token | Permite bursts controlados | Gestión de estado más compleja | APIs que necesitan tolerancia de burst | | Leaky Bucket | Las peticiones entran en una cola y se procesan a ritmo fijo | Muy suave tasa de salida | Puede añadir latencia, peticiones pueden ser soltadas | Entrega de webhook, procesamiento de trabajo | | Sliding Window Log | Almacena timestamp de cada petición | Más preciso | Alto uso de memoria a escala | Necesidades de baja-volumen, alta-precisión |

Para la mayoría de aplicaciones web, sliding window es el punto dulce. Es lo que Upstash usa por defecto, y es lo que recomendaría a menos que tengas una razón específica para elegir algo más.

Rate limiting en Astro y otros frameworks

Si estás construyendo con Astro, rate limiting funciona diferente porque Astro es principalmente un framework static-first. Pero con los server endpoints de Astro (disponibles en modo SSR), los conceptos son los mismos:

// 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' },
  });
};

Para aplicaciones desplegadas en edge en Cloudflare Workers, también podrías considerar las reglas de Rate Limiting built-in de Cloudflare, que operan a nivel de infraestructura y pueden manejar mucho más tráfico que soluciones a nivel de aplicación. Su Advanced Rate Limiting comienza en $0.05 por 10,000 peticiones buenas en el plan Business.

Monitoreo y depuración de errores 429 en producción

No puedes arreglar lo que no puedes ver. Aquí está mi lista de verificación para tratar con errores 429 en producción:

Cuando estás recibiendo 429s

  1. Verifica cuál API está devolviendo 429 — Mira la URL de la respuesta, no solo el código de estado
  2. Registra el encabezado Retry-After — Si es consistentemente muy largo, podrías necesitar un plan de nivel superior
  3. Audita tus patrones de petición — ¿Estás haciendo llamadas redundantes? ¿Puedes peticiones por lotes?
  4. Implementa caché — Usa stale-while-revalidate, caché de Redis, o Next.js ISR para reducir llamadas API
  5. Verifica si múltiples entornos comparten claves API — Esta es la causa de misterio 429 más común

Cuando estás enviando 429s

  1. Configura dashboards — Rastrea tasas de respuesta 429 en el tiempo
  2. Identifica los principales ofensores — ¿Cuáles direcciones IP o claves API están golpeando límites más?
  3. Revisa tus límites — ¿Son demasiado restrictivos? ¿Demasiado sueltos? Verifica tu capacidad de servidor y ajusta
  4. Siempre envía Retry-After — Sé un buen ciudadano API
  5. Incluye un mensaje de error útil — Dile al cliente cuál límite golpearon y cuándo reintentar

Una respuesta 429 bien redactada se ve así:

{
  "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"
  }
}

Esto es infinitamente más útil que solo { "error": "Too many requests" }.

Si estás lidiando con problemas persistentes de rate limiting en una arquitectura headless — ya sea durante compilaciones, en tiempo de ejecución, o ambas — podría valer la pena ponerse en contacto para discutir tu arquitectura. Hemos visto muchos de estos problemas a través de diferentes combinaciones de CMS y framework y usualmente hay una solución a nivel de patrón en lugar de solo parches de síntomas.

FAQ

¿Qué significa HTTP 429 Too Many Requests?

HTTP 429 es un código de estado que significa que has enviado demasiadas peticiones a un servidor dentro de un período de tiempo determinado. El servidor te está limitando por tasa — está pidiendo que vayas más lentamente. No es un error de autenticación o un error del servidor; tus peticiones son probablemente válidas, hay simplemente demasiadas. El servidor debería incluir un encabezado Retry-After diciéndote cuándo intentar de nuevo.

¿Cómo arreglo un error 429?

Si estás recibiendo errores 429 de una API, implementa backoff exponencial con jitter en tu lógica de reintento, reduce tu frecuencia de peticiones, añade caché para evitar llamadas redundantes, y respeta el encabezado Retry-After. Si estás golpeando el límite durante compilaciones, usa queuing de peticiones con límites de concurrencia. Si está sucediendo consistentemente, podrías necesitar actualizar a un plan API más alto con límites de tasa más generosos.

¿Qué es el encabezado Retry-After?

El encabezado Retry-After es enviado con una respuesta 429 (o 503) para decir al cliente cuánto tiempo esperar antes de hacer otra petición. Puede ser especificado como un número de segundos (e.g., Retry-After: 60) o como una fecha HTTP (e.g., Retry-After: Thu, 01 Jan 2026 00:00:00 GMT). No todas las APIs incluyen este encabezado, pero las bien diseñadas sí.

¿Cómo implemento rate limiting en Next.js?

Para desarrollo o despliegues de servidor único, puedes usar un Map en memoria para rastrear conteos de peticiones por dirección IP. Para despliegues serverless en producción en plataformas como Vercel, usa Upstash Redis con el paquete @upstash/ratelimit. Puedes aplicar rate limiting a nivel de ruta individual o a través de todas tus rutas API usando middleware de Next.js.

¿Cuál es la diferencia entre errores 429 y 503?

Un 429 Too Many Requests es específicamente sobre rate limiting — tu cliente está enviando demasiadas peticiones. Un 503 Service Unavailable significa que el servidor está sobrecargado o en mantenimiento y no puede manejar ninguna petición de nadie. Ambos pueden incluir un encabezado Retry-After, pero indican problemas muy diferentes. Un 429 es dirigido a ti; un 503 afecta a todos.

¿Puede rate limiting prevenir ataques DDoS?

Rate limiting es una capa de defensa contra ataques DDoS, pero no es suficiente por sí solo. Rate limiting a nivel de aplicación (como lo que implementarías en Next.js) puede manejar abuso moderado, pero un ataque DDoS serio necesita ser mitigado a nivel de infraestructura — usando servicios como Cloudflare, AWS Shield, o las protecciones built-in del proveedor de hosting. Piensa en rate limiting a nivel de app como un portero, e infraestructura de protección como las paredes de la fortaleza.

¿Qué límite de tasa debería establecer para mi API?

Depende completamente de tu caso de uso. Un punto de partida común para APIs públicas es 60 peticiones por minuto por IP, o 1,000 peticiones por hora por clave API. Para usuarios autenticados, podrías permitir más. La clave es monitorear patrones de uso actual, establecer límites que acomoden uso legítimo con algo de margen, y ajustar basado en datos reales. Comienza más restrictivo y afloja — es más fácil que apretar límites después de que los usuarios dependan de tasas más altas.

¿Por qué estoy recibiendo errores 429 durante mi compilación de sitio estático?

Los generadores de sitios estáticos como Next.js y Astro obtienen datos para cada página en tiempo de compilación. Si tienes cientos o miles de páginas, eso es cientos o miles de llamadas API en rápida sucesión. La mayoría de APIs de CMS tienen límites de tasa entre 5-20 peticiones por segundo. Usa p-limit o librerías similares para limitar concurrencia a 3-5 peticiones simultáneas, añade pequeños retrasos entre lotes, y considera usar compilaciones incrementales (ISR en Next.js, o Astro's incremental content collections) para evitar reconstruir todo a la vez.