Jeder Request zählt: Lastmanagement, 429 und Timeouts in der weclapp API
„Jeder Request zählt.“ Mit diesem Satz hat Christian Lanig (weclapp) am Community Day 2026 seinen Vortrag zur weclapp API eröffnet. Gemeint ist nicht, dass einzelne Requests teuer wären. Unnötige Requests kosten Effizienz, bei dir und bei allen anderen Integrationen auf demselben Mandanten. Wir fassen zusammen, was weclapp gezeigt hat, und ergänzen, was in den Folien fehlt: einen Client in JavaScript, der richtig auf Last reagiert, und Timeouts, die zueinander passen.
Suchst du nur die Grundlagen, erklärt unsere Seite Hat die weclapp API ein Rate Limit? Queue, 429 und Wait-Header in wenigen Minuten. Arbeitest du in Python, findest du Backoff, Wait-Header und abgestimmte Timeouts in weclappy 1.0 fertig umgesetzt.
Die ERP-API ist ein Produktivsystem
Lanig beginnt beim Kontext: Die ERP-API ist der Hub zwischen Onlineshop, Logistik, CRM, BI und weiteren Anwendungen. Schlechte API-Nutzung trifft deshalb ganze Prozesse, nicht nur die eine Integration. Die Grenze für parallele Requests gilt laut API-Doku je Mandant. Der nächtliche BI-Abgleich und der Scan am Packtisch teilen sich also dieselbe Kapazität.
Trotzdem setzt weclapp kein starres Rate Limit, und das soll laut Doku auch so bleiben. Ein Lastmanagement schützt Stabilität, Verfügbarkeit und Fairness, normale Nutzung soll davon nichts merken. Requests über der Grenze werden deshalb nicht gleich abgewiesen, sondern warten in einer Queue, derzeit bis zu 30 Sekunden. Erst danach antwortet die API mit HTTP 429 Too Many Requests. Solange die Wartezeit unter dieser Grenze bleibt, kosten dich kurze Spitzen also Zeit, aber keine Fehler.

Request-Sekunden: was Last wirklich ist
Nicht jeder Request kostet gleich viel. Die Last ergibt sich laut Lanig aus der Anzahl der Requests, ihrer Verarbeitungsdauer und der Parallelität. Sein Rechenbeispiel betrachtet ein Fenster von 60 Sekunden mit Lastfaktor 3, also 180 verfügbaren Request-Sekunden:
| Szenario laut Vortrag | Rechnung | Request-Sekunden | Ergebnis im Beispiel |
|---|---|---|---|
| Geringe Last | 500 Requests × 100 ms | 50 | unkritisch |
| Solide Last | 200 Requests × 400 ms × 2 Threads | 160 | unkritisch |
| Starke Last | 500 Requests × 100 ms × 4 Threads | 200 | 20 Sekunden Wartezeit |

Die 180 sind ein Rechenbeispiel, kein veröffentlichter Grenzwert. Das Prinzip dahinter steht aber auch in der API-Doku: Maßstab ist die effektive Request-Zeit. 500 Requests à 100 ms sind dort 50 Request-Sekunden und unkritisch. Kritisch werden viele rechenintensive Requests, die über mehrere Minuten laufen.
Für dein eigenes Design folgt daraus eine Reihenfolge. Spare zuerst Requests ein, senke dann ihre Dauer und dreh erst zuletzt an der Parallelität. Wo die Einsparung liegt, ordnet Lanig nach Symptomen:
| Symptom | Ursache | Lösung |
|---|---|---|
| Lastspitzen | Vollabgleich | Delta-Sync |
| gleichartige Aufrufe | Polling | Webhooks |
| viele Einzelrequests | N+1-Abfragen | referencedEntities |
Dazu kommen die Anti-Patterns, die er am häufigsten sieht: Vollabfragen im Intervall, permanentes Polling, N+1-Requests und parallele Schreibzugriffe, die miteinander konkurrieren. Sein Fazit: Viele Requests sind nicht dasselbe wie viel Nutzen.
429 ist ein Signal
Wer auf einen 429 sofort wiederholt, schickt genau die Last erneut, die ihn ausgelöst hat. Lanig setzt Exponential Backoff dagegen: erst 2 Sekunden warten, dann 4, dann länger. So bekommt das System Luft, und die API-Doku verlangt genau das.

Code zeigen die Folien nicht, deshalb hier ein kompakter Client in JavaScript (Node 18 oder neuer):
const BASE = "https://<dein-mandant>.weclapp.com/webapp/api/v2/";
const SAFE = new Set(["GET", "HEAD", "OPTIONS"]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function retryAfterMs(res) {
const seconds = Number(res.headers.get("retry-after")); // nur, falls gesetzt
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : null;
}
function backoffMs(attempt, { baseMs = 2000, capMs = 60000 } = {}) {
const ceiling = Math.min(capMs, baseMs * 2 ** attempt); // 2 s, 4 s, 8 s ... bis 60 s
return ceiling / 2 + Math.random() * (ceiling / 2); // Jitter: 50 bis 100 Prozent
}
function logQueue(method, path, res, started) {
const totalMs = Date.now() - started;
const waitMs = Number(res.headers.get("x-weclapp-wait-ms") ?? 0);
const entry = {
method,
path: path.split("?")[0], // keine Filter und IDs aus der Query ins Log
status: res.status,
totalMs,
waitMs,
ownMs: Math.max(0, totalMs - waitMs), // Bearbeitungszeit ohne Queue
reason: res.headers.get("x-weclapp-wait-reason") ?? "-",
};
(waitMs >= 2000 ? console.warn : console.log)(JSON.stringify(entry));
}
async function weclapp(path, { method = "GET", body, maxRetries = 5 } = {}) {
for (let attempt = 0; ; attempt++) {
const started = Date.now();
const res = await fetch(BASE + path, {
method,
headers: {
AuthenticationToken: process.env.WECLAPP_TOKEN,
Accept: "application/json",
...(body ? { "Content-Type": "application/json" } : {}),
"User-Agent": "meine-integration/1.0",
"X-Weclapp-Request-Timeout-Ms": "110000", // Server gibt vor dem Client auf
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(120_000), // Client-Timeout: 120 s
});
logQueue(method, path, res, started);
const retry = res.status === 429 && SAFE.has(method) && attempt < maxRetries;
if (!retry) return res; // über Writes entscheidet der aufrufende Code
await res.body?.cancel();
await sleep(Math.min(retryAfterMs(res) ?? backoffMs(attempt), 60_000));
}
}
Fünf Entscheidungen stecken in diesen Zeilen. Der Zufallsanteil (Jitter) sorgt dafür, dass mehrere Worker nach einem 429 nicht exakt gleichzeitig wiederkommen. Der Deckel begrenzt jede einzelne Pause auf 60 Sekunden, und nach fünf Versuchen gibt der Client den 429 an den aufrufenden Code zurück. Setzt der Server einen Retry-After-Header, hat er Vorrang, sonst greift der Backoff. Der Client-Timeout liegt bei 120 Sekunden, das Server-Limit per Header 10 Sekunden darunter, warum, steht im Abschnitt zu Timeouts. Und automatisch wiederholt werden nur GET, HEAD und OPTIONS, also Methoden, bei denen ein zweiter Versuch nichts verdoppeln kann.
Bei POST, PUT und DELETE ist das anders. Die Doku beschreibt beim 429, dass die Anfrage abgewiesen wurde, aber nicht, ob ein Write davor schon begonnen hat. Ein blinder Retry kann aus einer verlorenen Antwort eine doppelte Buchung machen. Prüf deshalb über eine stabile Geschäftskennung nach, ob die Änderung angekommen ist, und entscheide dann. Wie weclappy das in Python löst, steht im Artikel zu weclappy 1.0.
Last sichtbar machen
Musste ein Request warten, trägt die Antwort zwei Header, bei Erfolg genauso wie beim 429. Ohne Wartezeit fehlen sie.
| Header | Bedeutung |
|---|---|
X-Weclapp-Wait-Ms |
wie viele Millisekunden der Request vor der Bearbeitung gewartet hat |
X-Weclapp-Wait-Reason |
warum: concurrency (zu viele parallele Requests), load (hohe Gesamtlast) oder beides |
In Lanigs Beispiel kommt ein GET /party/id/123 mit X-Weclapp-Wait-Ms: 1000 und X-Weclapp-Wait-Reason: load zurück. Die Funktion logQueue im Client oben schreibt genau diese Werte in eine Logzeile pro Response und zieht die Wartezeit von der Gesamtzeit ab. So siehst du, ob ein Request von sich aus langsam war oder in der Queue stand. Werte die Zeilen als Perzentile aus statt nur als Durchschnitt: Der Mittelwert verdeckt gerade die Requests, die an der 30-Sekunden-Grenze kratzen.

Der Grund im Header sagt dir auch, wie du reagierst. Bei concurrency reicht es, weniger Requests gleichzeitig zu schicken. Bei load hilft nur weniger Gesamtvolumen, oder du verteilst schwere Aufgaben über mehr Zeit, wie es die Doku empfiehlt. Steigt die Wartezeit, drosselst du am besten schon vor dem ersten 429.
Timeouts abstimmen
Zu kurze Client-Timeouts lösen laut Lanig eine Kette aus: vorzeitige Abbrüche, mehr Retries, höhere Last und eine schlechtere Erfahrung für alle, die mit dem System arbeiten. Sein Leitsatz: „Nicht der schnellste Timeout gewinnt, sondern der am besten abgestimmte.“ Die Doku empfiehlt Timeouts von mindestens einer Minute, denn ein Request kann bis zu 30 Sekunden in der Queue stehen und braucht danach noch seine Bearbeitungszeit.
Ein Client-Timeout von genau 30 Sekunden ist deshalb eine Falle. Dein Client legt auf, während der Request noch wartet oder gerade bearbeitet wird. Bei einem Read ist das ärgerlich, bei einem Write gefährlich: Der Server kann die Änderung trotzdem übernehmen, und dein zweiter Versuch landet in einem Doppelversand oder einem Optimistic-Lock-Fehler.

Für den umgekehrten Fall, dass dein Client nicht so lange warten will, gibt es zwei Request-Header:
| Header | Bedeutung |
|---|---|
X-Weclapp-Wait-Timeout-Ms |
maximale Zeit bis zum Start der Bearbeitung |
X-Weclapp-Request-Timeout-Ms |
Best-Effort-Limit für den gesamten Request inklusive Wartezeit |
Beide Header können Timeouts nur verkürzen, nie über die Serverlimits hinaus verlängern. Ungültige, negative und Nullwerte ignoriert die API. Sind beide gesetzt, begrenzt der Request-Timeout auch die Wartezeit in der Queue. Überschreitet ein Request das Limit aus X-Weclapp-Request-Timeout-Ms, antwortet die API mit HTTP 400 und dem Fehlertyp request_timeout, eine klare Antwort statt eines abgerissenen Requests.
curl --compressed \
-H "AuthenticationToken: <dein-token>" \
-H "Accept: application/json" \
-H "X-Weclapp-Wait-Timeout-Ms: 5000" \
-H "X-Weclapp-Request-Timeout-Ms: 15000" \
"https://<dein-mandant>.weclapp.com/webapp/api/v2/party?pageSize=1&properties=id"
Daraus ergeben sich unsere Richtwerte. Sie sind eine Empfehlung von uns, keine Vorgabe von weclapp, und entsprechen den Standardwerten von weclappy. Die Regel dahinter: Das Server-Limit liegt knapp unter dem Client-Timeout, damit die request_timeout-Antwort ankommt, bevor dein Client auflegt. Ohne diesen Abstand geben beide Seiten im selben Moment auf, und du weißt nicht, was passiert ist.
| Einsatz | Client-Timeout | Wait-Timeout-Header | Request-Timeout-Header |
|---|---|---|---|
| Hintergrundjob, Sync, Export, Writes | 120 Sekunden | 30.000 ms | 110.000 ms |
| Interaktive Abfrage, ein Mensch wartet | 20 Sekunden | 5.000 ms | 15.000 ms |
Die zweite Zeile weicht bewusst von der Minuten-Empfehlung der Doku ab: Wenn ein Mensch auf die Antwort wartet, ist eine klare Absage nach 15 Sekunden besser als eine Minute Stillstand. Der Server bricht dann selbst ab und meldet es, dein Client wartet nur lange genug, um diese Meldung zu bekommen. Writes behandelst du anders, auch mit request_timeout: Die Doku lässt offen, ob ein Write danach zurückgerollt oder trotzdem gespeichert wurde. Wiederhole ihn deshalb nicht automatisch, sondern lies nach.
Checkliste
- Zähle die Calls pro Vorgang und schätze ihre Request-Sekunden.
- Streiche überflüssige Calls, bevor du an der Parallelität drehst.
- Ersetze Vollabgleich durch Delta-Sync, Polling durch Webhooks und N+1-Abfragen durch
referencedEntities. - Behandle 429 mit Exponential Backoff, Jitter und Deckel.
- Wiederhole automatisch nur GET, HEAD und OPTIONS, bei Writes liest du erst nach.
- Logge
X-Weclapp-Wait-MsundX-Weclapp-Wait-Reasonpro Response und drossle, bevor der 429 kommt. - Setze den Client-Timeout auf mindestens 60 Sekunden und das Server-Limit per Header knapp darunter. Kürzer nur bei interaktiven Reads.
Lanig schließt mit drei Sätzen, die als Zusammenfassung taugen: 429 ist ein Signal. Messen statt raten. Und die beste Integration sendet nur die Requests, die sie wirklich braucht.
Weiterlesen und nachbauen
Die Requests aus diesem Artikel kannst du mit der kostenlosen Postman Collection 4 weclapp direkt gegen deinen Mandanten ausprobieren. Welche Filter weclapp still ignoriert und wie du das prüfst, steht in Die stillen Fehler der weclapp API. Adaptive Parallelität, Retry-Budgets und Paginierung im Detail findest du im Artikel zu weclappy 1.0.
Willst du eine Integration auf Last und Fehlerfälle prüfen lassen, schauen wir gemeinsam drauf: wals.pro/termin.