HTTP 429 Too Many Requests: Wat Developers Missen Over Rate Limits
Je deploy gaat vrijdag om 16:00 uit. Monitoring kleurt rood op drie dashboards. Gebruikers zien 429-fouten op checkout, op login, op elke API-call die belangrijk is. Je rate limiter weigert requests — of erger nog, een third-party API weigert jou, en je retry-logica maakt het erger. De HTTP 429 Too Many Requests statuscode is net het enige wat tussen jou en een vrij weekend staat. De meeste developers weten dat 429 'langzamer gaan' betekent. Wat ze missen is welke requests ze moeten herhalen, wanneer ze exponentieel moeten terugvallen, en hoe je Retry-After headers instelt die echt de death spiral voorkomen. Dit is wat echt gebeurt wanneer je API begint met het weigeren van traffic.
Ik heb aan beide kanten van dit probleem gestaan. Ik ben de developer geweest die per ongeluk een CMS-API DDoS'te vanwege een verkeerd geconfigureerd buildproces, en ik ben degene geweest die rate limiting implementeerde om onze eigen servers te beschermen tegen ontspoorde clients. Beide ervaringen hebben me dingen geleerd die de docs niet behandelen. Laten we er doorheen gaan.
Wat betekent HTTP 429 eigenlijk?
HTTP 429 is gedefinieerd in RFC 6585, gepubliceerd in 2012. De spec is verrassend kort. Dit is de essentie: de gebruiker (of client) heeft te veel requests in een bepaalde hoeveelheid tijd gestuurd.
Dat is alles. Het is een rate limiting-respons. De server zegt: "Ik begreep je request, het is waarschijnlijk geldig, maar je moet langzamer gaan."
Dit is anders dan 403 Forbidden (je mag niet) of 503 Service Unavailable (de hele server worstelt). Een 429 is gericht. Het gaat om jouw requestsnelheid specifiek.
De respons MOET een Retry-After header bevatten die de client vertelt hoe lang het moet wachten voordat het opnieuw probeert. Ik zei "moet" omdat veel API's zich daar niet aan houden, wat het leven voor iedereen moeilijker maakt.
Waar je 429's in het wild ziet
- Third-party API's: Stripe, OpenAI, GitHub, Contentful, Sanity — ze hebben allemaal rate limits
- CDN's en hosting platforms: Vercel, Cloudflare, en AWS geven 429's terug als je hun edge rate limits raakt
- Je eigen API's: Als je rate limiting hebt geïmplementeerd (en je zou dat moeten doen)
- Buildprocessen: Statische site-generatie die een CMS-API voor elke pagina gebruikt, kan gemakkelijk rate limits triggeren
- Web scraping: Als je agressief data uit externe bronnen ophaalt
Veelvoorkomende oorzaken van 429-fouten
Laat me de scenarios uiteenzetten die ik echt in productie ben tegengekomen, ruwweg gerangschikt naar hoe vaak ze voorkomen.
1. Static Site Builds Die een Headless CMS Bombarderen
Dit is degene die teams die met headless architectures werken het meest treft. Je hebt een site met 2.000 pagina's, elk neemt data van je CMS. Je buildproces stuurt al die requests parallel, de CMS ziet een massale piek, en begint 429's terug te geven. Je build mislukt.
We zien dit regelmatig bij headless CMS-projecten. De fix omvat request queuing en concurrency limits, die ik hieronder zal behandelen.
2. Ontbrekende of Verbroken Caching
Als elke pagina-laad een verse API-call triggert omdat je caching layer niet werkt, bereik je snel rate limits — vooral met traffic spikes. Ik debugde ooit een Next.js-app waarbij revalidate per ongeluk op 0 was gezet, dus ISR was effectief uitgeschakeld. Elke bezoeker triggerde een nieuwe API-call naar Contentful. Het duurde ongeveer 45 minuten met echt traffic om 429's te beginnen krijgen.
3. Retry-Loops Zonder Backoff
Je code krijgt een error, probeert onmiddellijk opnieuw, krijgt nog een error, probeert onmiddellijk opnieuw... gefeliciteerd, je hebt een rate-limit-triggering-machine gebouwd. Ik heb dit patroon gezien in webhook handlers, achtergrondtaken, en zelfs client-side fetch calls.
4. Meerdere Services Die een API-sleutel Delen
Je staging environment, je production environment, je lokale dev setup, en je CI/CD pipeline gebruiken allemaal dezelfde API-sleutel. Elk ziet er apart goed uit, maar samen verbranden ze je rate limit budget.
5. Client-Side Fetch Zonder Debouncing
Een search-as-you-type feature die een API-call op elke keystroke triggert. Een dashboard dat elke 500ms polls. Een infinite scroll die fetches sneller triggert dan de gebruiker kan scrollen. Deze patronen kunnen absoluut 429's triggeren, vooral vermenigvuldigd over al je gebruikers.
6. Daadwerkelijk Misbruik of Aanval
Soms doet een 429 precies wat het hoort — je server beschermen tegen iemand die een onredelijk aantal requests stuurt. 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 je opnieuw moet proberen. Het kan in twee formats 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
De seconds-format is veel vaker. Het date-format 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 gokken.
Sommige API's sturen ook aanvullende rate limit headers:
| Header | Doel | Voorbeeld |
|---|---|---|
X-RateLimit-Limit |
Max requests toegestaan per window | 100 |
X-RateLimit-Remaining |
Requests resterend in huidige window | 0 |
X-RateLimit-Reset |
Unix timestamp wanneer het window reset | 1735689600 |
Retry-After |
Seconden om te wachten voordat je opnieuw probeert | 30 |
Controleer altijd op deze headers. Ze laten je slimmere retry-logica implementeren en zelfs proactief langzamer gaan voordat je de limit raakt.

Hoe je 429-fouten als Client Afhandelt
Als je degene bent die 429-fouten ontvangt, is dit hoe je ze correct afhandelt.
Exponential Backoff met Jitter
Dit is de gouden standaard. Wacht niet zomaar een vaste hoeveelheid tijd — verhoog de vertraging exponentieel met elke retry, en voeg wat willekeur (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 Buildprocessen
Voor statische site-generatie waar je honderden of duizenden API-calls 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+ wekelijkse npm downloads in 2026) is mijn go-to hiervoor. Je kunt ook een vertraging tussen requests 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 laten we wisselen naar de andere kant — je bouwt een API en moet deze beschermen. Als je bouwt met Next.js, is hier 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),
},
}
);
}
Production Rate Limiting met Upstash Redis
De in-memory aanpak valt uit elkaar wanneer je op serverless platforms zoals Vercel draait, omdat elke function invocation een ander instance kan raken. Je hebt een shared store nodig. Upstash Redis is de meest populaire keuze hiervoor in 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's gratis tier geeft je 10.000 requests/dag, wat meer dan voldoende is voor kleine projecten. Hun Pro plan begint bij €10/maand voor 500K dagelijkse commands vanaf begin 2026.
Middleware-Level Rate Limiting
Als je rate limiting over al je API routes wilt, is Next.js middleware de plek:
// 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-algoritmen zijn gelijk. Hier is hoe de belangrijkste zich verhouden:
| Algoritme | Hoe het Werkt | Voordelen | Nadelen | Beste Voor |
|---|---|---|---|---|
| Fixed Window | Telt requests in vaste tijdvensters (bv. per minuut) | Eenvoudig te implementeren | Burst op venstergrenzen kan 2x de limit toestaan | Eenvoudige API's, internal tools |
| Sliding Window | Telt requests over een rollende periode | Vloeierdere verdeling | Iets complexer, meer geheugen | Meeste production API's |
| Token Bucket | Tokens vullen met een constant rate, elke request kost een token | Staat gecontroleerde bursts toe | Complexer state management | API's die burst-tolerantie nodig hebben |
| Leaky Bucket | Requests gaan een queue in en worden met vaste rate verwerkt | Zeer vloeiende output rate | Kan latency toevoegen, requests kunnen worden gedropt | Webhook delivery, job processing |
| Sliding Window Log | Slaat timestamp van elke request op | Meest nauwkeurig | Hoog geheugengebruik op schaal | Low-volume, high-accuracy needs |
Voor de meeste web applications is sliding window het 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 bouwt met Astro, 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-gedeployde 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 verwerken dan application-level oplossingen. Hun Advanced Rate Limiting begint bij €0,05 per 10.000 goede requests op het Business plan.
Monitoring en Debugging van 429-fouten in Production
Je kunt niet fixen wat je niet kunt zien. Hier is mijn checklist voor het omgaan met 429-fouten in production:
Wanneer je 429's Ontvangt
- Controleer welke API 429 teruggeeft — Kijk naar de response URL, niet zomaar de statuscode
- Log de
Retry-Afterheader — Als het consistent erg lang is, heb je misschien een hoger tier plan nodig - Audit je request patronen — Maak je redundante calls? Kun je requests batchen?
- Implementeer caching — Gebruik
stale-while-revalidate, Redis caching, of Next.js ISR om API-calls te reduceren - Controleer of meerdere environments API-sleutels delen — Dit is de meest voorkomende "geheimzinnige" 429 oorzaak
Wanneer je 429's Stuurt
- Zet dashboards op — Track 429 response rates over tijd
- Identificeer top offenders — Welke IP adressen of API-sleutels raken limits het meest?
- Herzie je limits — Zijn ze te restrictief? Te los? Controleer je server capaciteit en pas aan
- Stuur altijd
Retry-After— Wees een goede API-burger - Voeg een nuttig error-bericht toe — Vertel de client welke limit ze hebben geraakt en wanneer ze opnieuw moeten proberen
Een goed uitgewerkte 429 response body ziet er zo 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 nuttiger dan gewoon { "error": "Too many requests" }.
Als je te maken hebt met aanhoudende rate limiting-problemen op een headless architecture — of het nu tijdens builds, bij runtime, of allebei is — kan het de moeite waard zijn om contact op te nemen om je architecture te bespreken. We hebben veel van deze problemen gezien over verschillende CMS en framework combinaties en meestal is er een pattern-level fix in plaats van gewoon de symptomen te bestrijken.
Veelgestelde Vragen
Wat betekent HTTP 429 Too Many Requests?
HTTP 429 is een statuscode die betekent dat je te veel requests in een bepaalde tijd naar een server hebt gestuurd. De server rate limiet je — het vraagt je om langzamer te gaan. Het is geen authentication error of een server error; je requests zijn waarschijnlijk geldig, er zijn gewoon te veel. De server moet een Retry-After header bevatten die je vertelt wanneer je opnieuw moet proberen.
Hoe fix ik een 429-fout?
Als je 429-fouten van een API ontvangt, implementeer exponential backoff met jitter in je retry-logica, reduceer je request frequentie, voeg caching toe om redundante calls te vermijden, en respecteer de Retry-After header. Als je de limit bereikt tijdens builds, gebruik request queuing met concurrency limits. Als het consistent gebeurt, moet je misschien upgraden naar een hoger API plan met meer gegeven rate limits.
Wat is de Retry-After header?
De Retry-After header wordt verzonden met een 429 (of 503) respons om de client te vertellen hoe lang het moet wachten voordat het nog een request maakt. Het kan worden opgegeven als een aantal seconden (bv. Retry-After: 60) of als een HTTP date (bv. 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 request counts per IP address bij te houden. Voor production serverless deployments op platforms zoals Vercel, gebruik je Upstash Redis met het @upstash/ratelimit package. Je kunt rate limiting op individual route level toepassen of over alle API routes met behulp van Next.js middleware.
Wat is het verschil tussen 429 en 503 fouten?
Een 429 Too Many Requests gaat specifiek over rate limiting — jouw client stuurt te veel requests. Een 503 Service Unavailable betekent dat de server overbelast is of onderhoud ondergaat en geen requests van iemand kan afhandelen. Beide kunnen een Retry-After header bevatten, maar ze geven verschillende problemen aan. Een 429 is gericht op jou; een 503 raakt iedereen.
Kan rate limiting DDoS-aanvallen voorkomen? Rate limiting is één laag verdediging tegen DDoS-aanvallen, maar het is niet voldoende op zichzelf. Application-level rate limiting (zoals wat je zou implementeren in Next.js) kan matig misbruik afhandelen, maar een ernstige DDoS-aanval moet op infrastructure level worden beperkt — met services als Cloudflare, AWS Shield, of je hosting provider's ingebouwde protecties. Denk aan app-level rate limiting als een portier, en infrastructure-level protectie als de vestingmuren.
Wat rate limit moet ik voor mijn API instellen? Het hangt volledig af van je use case. Een veelgebruikt startpunt voor publieke API's is 60 requests per minuut per IP, of 1.000 requests per uur per API-sleutel. Voor geverifieerde gebruikers kun je meer toestaan. De sleutel is het monitoren van echte gebruikspatronen, limits instellen die legitiem gebruik met wat speelruimte accommoderen, en aanpassen op basis van echte data. Begin meer restrictief en zak af — het is makkelijker dan limits strakker trekken nadat gebruikers afhankelijk zijn van hogere rates.
Waarom krijg ik 429-fouten tijdens mijn static site build?
Static site generators als Next.js en Astro halen data voor elke pagina op bij build time. Als je honderden of duizenden pagina's hebt, zijn dat honderden of duizenden API-calls in snelle opeenvolging. De meeste CMS API's hebben rate limits tussen 5-20 requests per seconde. Gebruik p-limit of soortgelijke libraries om concurrency op 3-5 gelijktijdige requests te limiteren, voeg kleine vertragingen tussen batches toe, en overweeg incremental builds (ISR in Next.js, of Astro's incremental content collections) om niet alles tegelijk opnieuw te bouwen.