HTTP 429 Too Many Requests: Causes, Fixes, and Rate Limiting
Je implementeert op vrijdagmiddag (ik weet het, ik weet het), alles ziet er goed uit, en dan licht je monitoring op als een kerstboom. Gebruikers krijgen 429 fouten. Je API weigert verzoeken. Of misschien is het andersom — je roept een API van derden aan en zij weigeren jou. Hoe dan ook, de HTTP 429 Too Many Requests statuscode is zojuist het belangrijkste iets in je dag geworden.
Ik ben aan beide zijden van dit probleem geweest. Ik ben de ontwikkelaar die per ongeluk een CMS API bombardeerde vanwege een verkeerd geconfigureerd bouwproces, en ik ben degene die rate limiting implementeerde om onze eigen servers tegen onbeheersbare clients te beschermen. Beide ervaringen leerden me dingen die de documentatie niet behandelt. Laten we alles doorlopen.
Inhoudsopgave
- Wat betekent HTTP 429 eigenlijk?
- Veelvoorkomende oorzaken van 429 fouten
- De Retry-After header uitgelegd
- Hoe 429 fouten als client af te handelen
- Rate limiting implementeren in Next.js API Routes
- Rate limiting strategieën vergeleken
- Rate limiting in Astro en andere frameworks
- Bewaking en debugging van 429 fouten in productie
- Veelgestelde vragen
Wat betekent HTTP 429 eigenlijk?
HTTP 429 is gedefinieerd in RFC 6585, gepubliceerd in 2012. De specificatie is verrassend kort. Dit is de essentie: de gebruiker (of client) heeft teveel verzoeken in een bepaalde tijd gestuurd.
Dat is het. Het is een rate limiting respons. De server zegt: "Ik begreep je verzoek, het is waarschijnlijk geldig, maar je moet langzamer gaan."
Dit verschilt van een 403 Forbidden (je mag niet) of een 503 Service Unavailable (de hele server heeft problemen). Een 429 is gericht. Het gaat om jouw verzoekfrequentie specifiek.
De respons MOET een Retry-After header bevatten die de client vertelt hoe lang te wachten voordat opnieuw geprobeerd wordt. Ik zei "moet" omdat veel APIs zich daar niet aan houden, wat iedereen het leven moeilijker maakt.
Waar je 429s in het wild ziet
- API's van derden: Stripe, OpenAI, GitHub, Contentful, Sanity — ze hebben allemaal rate limits
- CDN's en hosting platforms: Vercel, Cloudflare en AWS zullen 429s retourneren als je hun edge rate limits raakt
- Je eigen API's: Als je rate limiting hebt geïmplementeerd (en dat zou je moeten doen)
- Bouwprocessen: Static site generation die een CMS API voor elke pagina aanroept kan eenvoudig rate limits triggeren
- Web scraping: Als je agressief gegevens van externe bronnen ophaalt
Veelvoorkomende oorzaken van 429 fouten
Laat me de scenario's die ik daadwerkelijk in productie ben tegengekomen opsplitsen, ongeveer gerangschikt naar frequentie.
1. Static site builds die een headless CMS bombarderen
Dit is degene die teams die met headless architecturen werken het meest treft. Je hebt een site met 2.000 pagina's, elk needing data van je CMS. Je bouwproces vuurt al die verzoeken parallel af, de CMS ziet een enorme piek en begint 429s te retourneren. Je build faalt.
Wij zien dit regelmatig bij het werken aan headless CMS projecten. De fix omvat request queuing en gelijktijdigheidlimieten, die ik hieronder zal behandelen.
2. Ontbrekende of verbroken caching
Als elke page load een vers API-verzoek triggert omdat je caching layer niet werkt, zul je snel rate limits raken — vooral bij verkeerspieken. Ik debugde eens een Next.js app waar revalidate per ongeluk op 0 was gezet, wat betekende dat ISR effectief was uitgeschakeld. Elke bezoeker triggerde een nieuw API-verzoek naar Contentful. Het duurde ongeveer 45 minuten echte traffic voordat we 429s kregen.
3. Retry loops zonder backoff
Je code krijgt een fout, probeert onmiddellijk opnieuw, krijgt nog een fout, probeert onmiddellijk opnieuw... gefeliciteerd, je hebt een rate-limit-triggering machine gebouwd. Ik heb dit patroon gezien in webhook handlers, background jobs en zelfs client-side fetch calls.
4. Meerdere services delen een API-sleutel
Je staging environment, je productie environment, je lokale dev setup en je CI/CD pipeline gebruiken allemaal dezelfde API-sleutel. Elk ziet er individueel prima uit, maar gezamenlijk branden ze door je rate limit budget.
5. Client-side fetch zonder debouncing
Een search-as-you-type feature die een API-verzoek afvuurt bij elke toetsaanslag. Een dashboard dat elke 500ms poll. Een infinite scroll die fetches triggert sneller dan de gebruiker kan scrollen. Deze patronen kunnen absoluut 429s triggeren, vooral wanneer vermenigvuldigd over al je gebruikers.
6. Werkelijk misbruik of aanval
Soms doet een 429 exact wat het hoort te doen — je server beschermen tegen iemand die een onredelijk aantal verzoeken verstuurt. Bots, credential stuffing, scraping — rate limiting is je eerste verdedigingslinie.
De Retry-After header uitgelegd
De Retry-After header is de manier van de server om je precies te vertellen wanneer opnieuw te proberen. Het kan in twee formaten komen:
Seconden om te wachten:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Specifieke datum/tijd:
HTTP/1.1 429 Too Many Requests
Retry-After: Thu, 01 Jan 2026 00:00:00 GMT
Het secondes formaat is veel vaker voorkomen. Het datum formaat gebruikt HTTP-date zoals gedefinieerd in RFC 7231.
Hier is wat de meeste tutorials je niet zullen vertellen: veel API's sturen Retry-After helemaal niet, of ze sturen het inconsistent. OpenAI's API bevat het over het algemeen. GitHub's API bevat het samen met X-RateLimit-Reset. Veel kleinere API's sturen gewoon een naakte 429 en laten je gissen.
Some APIs also send additional rate limit headers:
| Header | Doel | Voorbeeld |
|---|---|---|
X-RateLimit-Limit |
Max verzoeken toegestaan per window | 100 |
X-RateLimit-Remaining |
Verzoeken resterend in huidiging window | 0 |
X-RateLimit-Reset |
Unix timestamp wanneer het window reset | 1735689600 |
Retry-After |
Seconden om te wachten voordat opnieuw geprobeerd wordt | 30 |
Controleer altijd op deze headers. Ze laten je slimmere retry logic implementeren en kunnen zelfs proactief afremmen voordat je de limit raakt.
Hoe 429 fouten als client af te handelen
Wanneer je 429 fouten ontvangt, hier is hoe je ze juist afhandelt.
Exponentiële backoff met jitter
Dit is de gouden standaard. Wacht niet gewoon een vaste hoeveelheid tijd — verhoog de vertraging exponentieel met elke retry, en voeg wat willekeurigheid (jitter) toe om thundering herd problemen te voorkomen.
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 voor bouwprocessen
Voor static site generation waarbij je honderden of duizenden API-verzoeken moet doen, gebruik je een queue met concurrency control:
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}`))
)
);
De p-limit library (2,5M+ weekly npm downloads in 2025) is mijn go-to voor dit. Je kunt ook een vertraging tussen verzoeken toevoegen:
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}`);
})
)
);
Rate limiting implementeren in Next.js API Routes
Nu gaan we naar de andere kant — je bouwt een API en moet deze beschermen. Als je met Next.js bouwt, hier is hoe je rate limiting aan je API routes toevoegt.
Eenvoudige in-memory rate limiter
Voor een single-server deployment of tijdens development werkt dit:
// 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,
};
};
}
Gebruiken in een 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),
},
}
);
}
Productie rate limiting met Upstash Redis
De in-memory aanpak werkt niet meer als je op serverless platforms zoals Vercel runt, omdat elke function invocation misschien een ander instance raakt. Je hebt een gedeelde opslag nodig. Upstash Redis is de meest populaire keuze hiervoor in 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's gratis laag geeft je 10.000 verzoeken/dag, wat genoeg is voor kleine projecten. Hun Pro plan begint op $10/month voor 500K dagelijkse commands vanaf begin 2025.
Middleware-level rate limiting
Als je rate limiting over al je API routes wilt, is Next.js middleware de plaats:
// 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*',
};
Rate limiting strategieën vergeleken
Niet alle rate limiting algoritmes zijn gelijk. Hier is hoe de belangrijkste zich verhouden:
| Algoritme | Hoe het werkt | Voordelen | Nadelen | Beste voor |
|---|---|---|---|---|
| Fixed Window | Telt verzoeken in vaste tijdvensters (bijv. per minuut) | Eenvoudig te implementeren | Burst aan venster grenzen kan 2x de limit toestaan | Eenvoudige API's, interne tools |
| Sliding Window | Telt verzoeken over een rolling periode | Vloottere distributie | Iets complexer, meer geheugen | Meeste productie API's |
| Token Bucket | Tokens vullen zich met een constante snelheid, elk verzoek kost een token | Maakt gecontroleerde bursts mogelijk | Complexer state management | API's die burst tolerantie nodig hebben |
| Leaky Bucket | Verzoeken voeren een queue in en worden met vaste snelheid verwerkt | Zeer vlootte output snelheid | Kan latency toevoegen, verzoeken kunnen gedropt worden | Webhook delivery, job processing |
| Sliding Window Log | Slaat timestamp van elk verzoek op | Meest nauwkeurig | Hoog geheugengebruik op schaal | Low-volume, high-accuracy needs |
Voor de meeste web applicaties is sliding window de sweet spot. Het is wat Upstash standaard gebruikt, en het is wat ik zou aanbevelen tenzij je een specifieke reden hebt om iets anders te kiezen.
Rate limiting in Astro en andere frameworks
Als je met Astro bouwt, werkt rate limiting anders omdat Astro primair een static-first framework is. Maar met Astro's server endpoints (beschikbaar in SSR mode) zijn de concepten dezelfde:
// 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' },
});
};
Voor edge-deployed applicaties op Cloudflare Workers zou je ook Cloudflare's ingebouwde Rate Limiting rules kunnen overwegen, die op infrastructure level werken en veel meer traffic kunnen afhandelen dan application-level oplossingen. Hun Advanced Rate Limiting begint op €0,05 per 10.000 goede verzoeken op het Business plan.
Bewaking en debugging van 429 fouten in productie
Je kunt niet repareren wat je niet kunt zien. Hier is mijn checklist voor het omgaan met 429 fouten in productie:
Wanneer je 429s ontvangt
- Controleer welke API 429 retourneert — Kijk naar de response URL, niet alleen de statuscode
- Log de
Retry-Afterheader — Als het consistent erg lang is, heb je misschien een hoger tier plan nodig - Controleer je verzoekpatronen — Maak je redundante oproepen? Kun je verzoeken batchelen?
- Implementeer caching — Gebruik
stale-while-revalidate, Redis caching of Next.js ISR om API calls te verminderen - Controleer of meerdere environments dezelfde API-sleutels delen — Dit is de meest voorkomende "mystery" 429 oorzaak
Wanneer je 429s verstuurt
- Zet dashboards op — Volg 429 response rates over tijd
- Identificeer top offenders — Welke IP adressen of API-sleutels raken limits het meest?
- Controleer je limieten — Zijn ze te restrictief? Te los? Controleer je server capaciteit en stel bij
- Stuur altijd
Retry-After— Wees een goede API citizen - Voeg een nuttig error bericht toe — Vertel de client welke limit ze raken en wanneer opnieuw te proberen
Een goed gevormd 429 response body ziet er als volgt uit:
{
"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"
}
}
Dit is oneindig veel nuttig dan gewoon { "error": "Too many requests" }.
Als je te maken hebt met aanhoudende rate limiting problemen op een headless architectuur — of het nu tijdens builds, runtime of beide — kan het de moeite waard zijn om contact op te nemen en je architectuur te bespreken. We hebben veel van deze problemen gezien over verschillende CMS en framework combinaties en er is meestal een pattern-level fix in plaats van gewoon de symptomen afplakken.
Veelgestelde vragen
Wat betekent HTTP 429 Too Many Requests?
HTTP 429 is een statuscode die betekent dat je teveel verzoeken naar een server hebt gestuurd binnen een bepaalde tijdsperiode. De server rate limit je — hij vraagt je om langzamer te gaan. Het is geen authenticatie fout of server fout; je verzoeken zijn waarschijnlijk geldig, er zijn er gewoon teveel van. De server zou een Retry-After header moeten bevatten die je vertelt wanneer opnieuw te proberen.
Hoe fix ik een 429 fout?
Als je 429 fouten van een API ontvangt, implementeer exponentiële backoff met jitter in je retry logic, verklein je verzoekfrequentie, voeg caching toe om redundante oproepen te voorkomen en respecteer de Retry-After header. Als je de limit raakt tijdens builds, gebruik request queuing met concurrency limits. Als het consistent gebeurt, heb je misschien een upgrade naar een hoger API plan met royalere rate limits nodig.
Wat is de Retry-After header?
De Retry-After header wordt verstuurd met een 429 (of 503) respons om de client te vertellen hoe lang te wachten voordat nog een verzoek wordt gedaan. Het kan worden gespecificeerd als aantal seconden (bijv. Retry-After: 60) of als HTTP datum (bijv. Retry-After: Thu, 01 Jan 2026 00:00:00 GMT). Niet alle API's bevatten deze header, maar de goed ontworpen wel.
Hoe implementeer ik rate limiting in Next.js?
Voor development of single-server deployments kun je een in-memory Map gebruiken om verzoek counts per IP adres bij te houden. Voor productie serverless deployments op platforms zoals Vercel, gebruik Upstash Redis met het @upstash/ratelimit package. Je kunt rate limiting toepassen op individuele route level of over alle API routes met Next.js middleware.
Wat is het verschil tussen 429 en 503 fouten?
Een 429 Too Many Requests gaat specifiek over rate limiting — je client stuurt teveel verzoeken. Een 503 Service Unavailable betekent dat de server overbelast is of onderhoud ondergaat en verzoeken van iedereen niet kan afhandelen. Beide kunnen een Retry-After header bevatten, maar ze geven zeer verschillende problemen aan. Een 429 is gericht op jou; een 503 treft iedereen.
Kan rate limiting DDoS aanvallen voorkomen? Rate limiting is één verdedigingslaag tegen DDoS aanvallen, maar het is op zichzelf niet voldoende. Application-level rate limiting (zoals wat je in Next.js zou implementeren) kan matig misbruik afhandelen, maar een serieuze DDoS aanval moet op infrastructure level worden beperkt — met services zoals Cloudflare, AWS Shield of je hosting provider's ingebouwde beschermingen. Beschouw app-level rate limiting als een uitsmijter en infrastructure-level bescherming als de vestingmuren.
Welke rate limit zou ik voor mijn API moeten instellen? Het hangt volledig van je use case af. Een gemeenschappelijk startpunt voor openbare API's is 60 verzoeken per minuut per IP of 1.000 verzoeken per uur per API-sleutel. Voor geverifieerde gebruikers zou je meer kunnen toestaan. De sleutel is echte gebruikspatronen monitoren, limieten stellen die legitiem gebruik met wat speelruimte accommoderen, en aanpassen op basis van echte gegevens. Begin meer restrictief en verruim — het is gemakkelijker dan limieten aantrekken nadat gebruikers op hogere tarieven vertrouwen.
Waarom krijg ik 429 fouten tijdens mijn static site build?
Static site generators zoals Next.js en Astro halen gegevens voor elke pagina op build time. Als je honderden of duizenden pagina's hebt, dat zijn honderden of duizenden API-verzoeken in snelle opeenvolging. De meeste CMS API's hebben rate limits tussen 5-20 verzoeken per seconde. Gebruik p-limit of vergelijkbare libraries om gelijktijdigheid op 3-5 gelijktijdige verzoeken in te stellen, voeg kleine vertragingen tussen batches toe en overweeg incremental builds te gebruiken (ISR in Next.js of Astro's incremental content collections) om niet alles tegelijk opnieuw op te bouwen.