Die Content API ist großzügig, aber kein Fass ohne Boden. Ein paar einfache Limits halten sie für alle schnell — und eine Website, die vernünftig cacht, stößt nie daran. Betrachte diese Seite als die Tempolimit-Schilder an einer Straße, auf der du fast immer deutlich langsamer unterwegs bist.
Die Limits
| Was gezählt wird | Limit | Was darüber passiert |
|---|---|---|
| Requests von einer IP-Adresse | 300 / Minute | Diese IP bekommt 429, bis die Minute um ist. |
| Requests mit einem Site-Key (von allen IPs zusammen) | 600 / Minute | Requests mit diesem Key bekommen 429, bis die Minute um ist. |
| Requests mit ungültigem Key von einer IP | 20 / Minute | Die IP wird für den Rest der Minute gesperrt — selbst Requests mit gültigem Key bekommen 429. |
Feste Ein-Minuten-Fenster
Die Zähler arbeiten in festen Ein-Minuten-Fenstern, nicht mit einem gleitenden Durchschnitt. Requests füllen den Zähler, und wenn die Minute um ist, startet er wieder bei null. Zwei praktische Folgen:
- Ein Burst ganz am Anfang einer Minute ist okay, solange die Summe für diese Minute unter dem Limit bleibt.
- Hast du einmal ein
429bekommen, hilft in derselben Minute nichts mehr. Warten auf die nächste schon.
Welches Limit du zuerst erreichst
Das hängt davon ab, woher deine Requests kommen:
- Server-Side Rendering auf einer Maschine. Jeder Request geht von der IP deines Servers raus, also sind die 300 pro IP die Decke, an die du zuerst stößt. Mehrere Server haben jeweils ihre eigenen 300, teilen sich aber zusammen trotzdem die 600 des Keys.
- Fetchen im Browser. Jeder Besucher hat seine eigene IP, das Limit pro IP spielt also fast nie eine Rolle. Aber alle Besucher teilen sich einen Site-Key, und der Key bekommt 600 pro Minute für die gesamte Website. Der Browser-Cache hilft jedem Besucher einzeln, nicht der Masse.
So sieht ein 429 aus
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Cache-Control: no-store
{"message":"rate_limit_exceeded"}Das ist alles. Es gibt keinen Retry-After-Header und auch keine RateLimit-*-Header — such also nicht danach. Weil die Fenster feste Minuten sind, ist die Regel einfach: ungefähr eine Minute warten (mit etwas Zufall, siehe unten) und es noch mal versuchen.
Retry mit Backoff und Jitter
Ein kleiner Wrapper um fetch, der bei jeder Art von Fehler das Richtige tut:
- 429 — das Fenster aussitzen: etwa 60 Sekunden plus ein paar zufällige Sekunden.
- 5xx und Netzwerkfehler — exponentielles Backoff mit Jitter: ungefähr 0,5–1 s, dann 1–2 s, dann 2–4 s.
- Jeder andere 4xx — überhaupt keine Retries. Warten behebt keinen Tippfehler in einem Marker.
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
type RetryOptions = {
retries?: number; // wie viele zusätzliche Versuche
maxWaitMs?: number; // nie länger schlafen als das; stattdessen den Fehler zurückgeben
};
export async function fetchWithRetry(
url: string,
init: RequestInit = {},
{ retries = 3, maxWaitMs = 70_000 }: RetryOptions = {},
): Promise<Response> {
for (let attempt = 0; ; attempt++) {
let res: Response | undefined;
let error: unknown;
try {
res = await fetch(url, init);
// 2xx, 304 oder ein 4xx, den Warten nicht behebt (400, 401, 404…): unverändert zurückgeben
if (res.status !== 429 && res.status < 500) return res;
} catch (err) {
error = err; // Netzwerkprobleme: DNS, Timeout, Verbindungsabbruch
}
const wait =
res?.status === 429
? 60_000 + Math.random() * 5_000 // das Fenster ist eine Minute: aussitzen, plus Jitter
: 2 ** attempt * 500 * (1 + Math.random()); // 5xx / Netzwerk: 0,5–1 s, 1–2 s, 2–4 s…
if (attempt >= retries || wait > maxWaitMs) {
if (res) return res; // der Aufrufer soll den 429 / 5xx sehen
throw error;
}
await sleep(wait);
}
}Nutze ihn geduldig, wo niemand wartet, und ungeduldig, wo ein Mensch wartet:
const headers = { 'x-crm-key': process.env.CRM_KEY! };
const url = 'https://back.sitecog.com/content/v1/pages/home?lang=en';
// Ein Build-Skript oder ein Hintergrundjob: wartet einen 429 gern aus
const res = await fetchWithRetry(url, { headers });
// Eine Seite für einen Besucher rendern: niemand wartet eine Minute auf eine Seite.
// Ein 429 kommt sofort zurück, und du lieferst die Kopie aus, die du vorher gecacht hast.
const quick = await fetchWithRetry(url, { headers }, { retries: 1, maxWaitMs: 2_000 });Die SSR-Rechnung: Wie viele Requests macht dein Server?
Rechnen wir mal. Angenommen, ein typischer Seiten-Render holt zwei Dinge: die Seite selbst (/v1/pages/about) und den gemeinsamen Header und Footer (/v1/pages/common). Das sind zwei Requests pro Seitenaufruf. Die Website hat 30 Seiten in 2 Sprachen.
| Ansatz | API-Requests pro Minute | Wo es bricht |
|---|---|---|
| Kein Cache, Seite + common bei jedem Aufruf | 2 × Seitenaufrufe | 150 Aufrufe / Minute — das sind 2,5 Besucher pro Sekunde, ein Newsletter reicht |
| Kein Cache, jeder Block einzeln geholt (sagen wir 20 Blöcke) | 20 × Seitenaufrufe | 15 Aufrufe / Minute — eine belebte Mittagspause |
revalidate: 60, Seite + common | höchstens 30 × 2 + 1 × 2 = 62 | nie — dieselben 62 bei 10 Aufrufen oder 100.000 |
Die letzte Zeile ist der ganze Witz: Mit einem serverseitigen Cache wird jede eindeutige URL höchstens einmal pro Minute geholt, deine API-Nutzung ist also durch die Größe deiner Website gedeckelt, nicht durch ihre Beliebtheit. Viral gehen ist dann kein Infrastrukturproblem mehr.
Behalte deine Builds im Blick
Statische Generierung ist von Natur aus ein Burst: Ein Build, der 400 Seiten in 2 Sprachen parallel rendert, kann von einer Maschine in wenigen Sekunden 800 Requests abfeuern — weit über 300 pro Minute. Begrenz die Parallelität auf eine Handvoll Seiten gleichzeitig und nutze das geduldige fetchWithRetry von oben, damit ein 429 den Build eine Minute pausieren lässt, statt ihn scheitern zu lassen.
So bleibst du weit weg von den Limits
- Cache bei dir.
revalidatein Next.js, ISR oder ein kleiner In-Memory-Cache auf einem beliebigen Node-Server. Rezepte stehen auf der Seite Caching. - Ein Seiten-Request statt vieler Blöcke.
/v1/pages/:markerliefert jede Section und jeden Block auf einen Rutsch. Pizza Stück für Stück zu bestellen macht nur dem Lieferdienst Spaß. - Gemeinsamen Content einmal holen. Header- und Footer-Blöcke auf einer
common-Seite sind für jede Route gleich — lass einen einzigen gecachten Request sie alle bedienen. - Nutze ETags. Sie senken nicht die Zahl der Requests, machen aber jeden wiederholten Request in Sachen Bandbreite fast kostenlos. Siehe ETag und 304.
- Kein Polling. Die API jede Sekunde zu fragen, ob sich etwas geändert hat, ist genau der Traffic, für den es Limits gibt. Ein 60-Sekunden-Cache gibt der Redaktion ein ausreichend schnelles Feedback, und der Live-Modus ein sofortiges.
- Halte deine Keys sauber. Ein altes Preview-Deployment oder ein vergessener Cronjob mit widerrufenem Key, der auf demselben Server wie die Produktion läuft, kann die 20 Requests mit ungültigem Key in einer Minute verbrauchen — und dann bekommt auch die Produktion auf dieser IP
429. Ein fauler Apfel verdirbt wirklich den ganzen Korb.
curl -s -o /dev/null -w "%{http_code}\n" \
"https://back.sitecog.com/content/v1/langs" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"
# 200 — alles gut, 401 — repariere den Key, bevor irgendetwas anfängt, ihn zu wiederholenTrotzdem ein 429 bekommen und willst wissen, was sonst noch schiefgehen kann? Jeder Status steht auf der Seite Fehler.