Requests parallel ausführen
Was du lernst
Wie du einen großen Batch von FourA-Requests gleichzeitig ausführst, ohne 429-Fehler zu sammeln: Begrenze die Anzahl offener Aufrufe und reagiere auf Ablehnungen, anstatt sie blind zu wiederholen.
Voraussetzungen
- Ein FourA API-Key (hier erhältlich)
- Python 3.9+ mit
requestsoder Node.js 18+
Die Limits, gegen die du arbeitest
Dein Plan enthält zwei Obergrenzen pro Endpoint: wie viele Requests gleichzeitig laufen dürfen und wie viele pro Minute starten dürfen (Browser hat ein Tageskontingent statt eines Minutenkontingents). Single, Proxy und Browser haben jeweils eigene Limits, die im Tab Limits & Features unter Usage & Limits aufgeführt sind.
Requests, die das Nebenläufigkeitslimit überschreiten, werden als HTTP 429 mit X-FourA-Limit: plan_limit_concurrency und dem Limit im Body beantwortet:
{
"error": "Concurrency limit reached: your plan allows 50 simultaneous single request(s). Retry when an in-flight request finishes.",
"reason": "plan_limit_concurrency",
"documentation": "https://foura.ai/prices",
"limit": 50,
"in_flight": 51,
"retry_after_seconds": 1
}
Der Request, der das Minutenlimit überschreitet, wird mit X-FourA-Limit: plan_limit_rate beantwortet, und retry_after_seconds gibt an, wie lange es dauert, bis ein weiterer Request durchgeht (1 bis 120 Sekunden).
Beide Ablehnungen erfolgen sofort. FourA reiht den Aufruf nicht in eine Warteschlange ein, um ihn später zurückzugeben; es wird also nichts verbraucht und nichts berechnet. Ein abgelehnter Aufruf zählt nicht für die gleitende Minute, sodass ein Account, der sein Limit leicht überschreitet, weiterhin das volle Limit ausschöpfen kann. Ein Retry-Storm erhält eine eigene Antwort: Wenn weiterhin abgelehnte Aufrufe mit mehr als dem Doppelten deines Minutenlimits eingehen, wird jeder Aufruf an diesem Endpoint mit cooldown: true und retry_after_seconds: 30 abgelehnt; pausiere also für diese 30 Sekunden. Vollständige Feldreferenz: Rate Limits.
Zwei wichtige Punkte werden oft übersehen:
- Ein
POST /api/auto/-Aufruf belegt selbst keinen Slot, aber jeder Single-, Proxy- und Browser-Sub-Call, den er für dich ausführt, schon. Ein Auto-Aufruf kann mehr als einen Slot belegen, während seine Eskalationsleiter läuft. (In deinem Dashboard ist der Auto-Aufruf der Request; seine Sub-Calls sind seine Versuche.) - Ein Browser-Request belegt seinen Slot so lange, wie das Rendern der Seite dauert, was deutlich länger ist als bei einem Single-Request. Ein Batch von Browser-Aufrufen erreicht sein Limit mit weniger Requests als ein Batch von Single-Aufrufen.
Schritt 1: Begrenze deine eigene Nebenläufigkeit
Wähle eine Zahl unterhalb des Limits für den Endpoint, den du aufrufst, und halte sie ein. Ein Worker-Pool erledigt das in einer Zeile:
import os
import concurrent.futures
import requests
API = "https://eu.api.foura.ai/api/single/"
KEY = os.environ["FOURA_API_KEY"]
HEADERS = {"X-API-Key": KEY, "Content-Type": "application/json"}
# Below your plan's Single concurrency, so a slow request never pushes the batch over it.
MAX_IN_FLIGHT = 30
def fetch_one(url):
resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
return url, resp.status_code, resp.json()
def fetch_all(urls):
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_IN_FLIGHT) as pool:
for outcome in concurrent.futures.as_completed(pool.submit(fetch_one, u) for u in urls):
results.append(outcome.result())
return results
max_workers ist der gesamte Mechanismus. Der Pool hat nie mehr Aufrufe gleichzeitig offen. So kann der Batch eine Million URLs umfassen und bleibt trotzdem unter dem Limit.
Lass Puffer. Wenn zwei Prozesse auf deiner Seite dasselbe Konto nutzen, selbst mit verschiedenen API-Keys, teilen sie sich ein Limit. Gib jedem von ihnen daher die Hälfte. Wenn ein schnelles Ziel dem Pool erlaubt, Requests schneller abzuschließen als es pro Minute zulässig ist, drossle die Worker, um auch unter dem Minutenlimit zu bleiben.
Schritt 2: Backoff statt direktem erneuten Senden
Kommt es dennoch zu einer Ablehnung, solltest du den Batch keinesfalls sofort erneut senden. Jeder Aufruf darin wird erneut abgelehnt, und die Retries stapeln sich auf den Requests, die bereits liefen.
Warte zuerst. Die Wartezeit steht im Header Retry-After und in retry_after_seconds im Body:
import time
# Plan limits that no short wait will clear.
STOP_ON = {"plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", "plan_limit_feature", "plan_limit_premium"}
def fetch_one(url, attempts=4):
for attempt in range(attempts):
resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
limit = resp.headers.get("X-FourA-Limit")
if limit in STOP_ON:
raise RuntimeError(f"stopped by {limit}")
if resp.status_code not in (429, 503):
return url, resp.status_code, resp.json()
header = resp.headers.get("Retry-After")
if header and header.isdigit():
wait = int(header)
else:
body = resp.json()
wait = body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
time.sleep(wait)
return url, 429, {"error": "still refused after retries"}
Füge Jitter hinzu, wenn du viele Worker betreibst. Ohne ihn wacht jeder in derselben Sekunde abgewiesene Worker in derselben Sekunde wieder auf und wird erneut abgewiesen.
import random
time.sleep(wait + random.uniform(0, 0.5))
Schritt 3: Dasselbe in Node
const API = 'https://eu.api.foura.ai/api/single/';
const HEADERS = {
'X-API-Key': process.env.FOURA_API_KEY,
'Content-Type': 'application/json',
};
const MAX_IN_FLIGHT = 30;
async function fetchOne(url) {
const resp = await fetch(API, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ method: 'GET', url }),
});
return { url, status: resp.status, body: await resp.json() };
}
async function fetchAll(urls) {
const queue = [...urls];
const results = [];
async function worker() {
while (queue.length) {
results.push(await fetchOne(queue.pop()));
}
}
await Promise.all(
Array.from({ length: Math.min(MAX_IN_FLIGHT, urls.length) }, worker)
);
return results;
}
Eine feste Anzahl von Workern, die aus einer einzigen Queue abrufen, hält genau so viele Aufrufe offen, unabhängig von der Batch-Größe.
Schritt 4: Beobachte deinen tatsächlichen Verbrauch
Öffne Nutzung & Limits im Dashboard. Die Live-Zähler neben deinen Limits zeigen Concurrency, Rate pro Minute und Browser-Requests pro Tag in Echtzeit. So kannst du MAX_IN_FLIGHT anhand eines realen Laufs dimensionieren statt zu raten.
Das Aktivitätsprotokoll erfasst auch Ablehnungen. Ein Lauf, der mit der richtigen Anzahl an Zeilen, aber verstreuten rate_limit-Ergebnissen endete, hat sich selbst gedrosselt.
Häufige Fehler
- Die gesamte Liste auf einmal ausführen.
asyncio.gatherüber 5.000 URLs oderPromise.allüber ein unbegrenztes Array öffnet 5.000 Aufrufe. Begrenze den Pool, nicht die Liste. - Parallele Retries. Jeden abgelehnten Aufruf im Moment der Ablehnung sofort erneut zu senden, wiederholt den Burst, der die Ablehnung verursacht hat. Warte die
Retry-Afterab und füge Jitter hinzu. - Ein Tages- oder Periodenkontingent als Wartezeit behandeln.
plan_limit_browser_dailywird um Mitternacht UTC zurückgesetzt,plan_limit_creditsundplan_limit_bandwidtham Ende des Abrechnungszeitraums. Stoppe den Lauf und liesresets_ataus dem Body, falls vorhanden. - Auto-Aufrufe jeweils als einen Slot zählen. Die Kette innerhalb von
/api/auto/führt echte Unteraufrufe aus, und genau diese zählen für die Limits. - Einen Key über Prozesse hinweg teilen, ohne das Budget aufzuteilen. Die Obergrenzen gelten pro Account, nicht pro Prozess.
- Einen einzigen Pool für jeden Endpoint dimensionieren. Single, Proxy und Browser haben jeweils eigene Concurrency-Werte. Ein für Single dimensionierter Pool überschreitet ein kleineres Browser-Limit.
Verwandte Themen
- Rate Limits: Jedes Ablehnungsmuster und was die einzelnen Felder bedeuten
- Response Headers:
X-FourA-LimitundRetry-After - Request Outcomes: Warum ein
rate_limit-Ergebnis nie abgerechnet wird - Nutzung & Limits: Wo sich die Live-Zähler und die Werte deines Plans befinden
- Smart Fetch (Auto): Wie aus einem einzelnen Auto-Aufruf mehrere Unteraufrufe entstehen