Requests parallel ausführen
Was du lernst
Wie du einen grossen Batch von FourA-Requests gleichzeitig ausführst, ohne 429-Fehler zu erhalten: durch Begrenzen der offenen Calls und durch Reagieren auf Ablehnungen statt wiederholtem Senden.
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 Werte, die im Tab Limits & Features unter Usage & Limits aufgeführt sind.
Requests, die das Concurrency-Limit ü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 Aufruf, der das Minutenlimit überschreitet, wird mit X-FourA-Limit: plan_limit_rate und einem retry_after_seconds bis zum Ende der Minute zurückgewiesen.
Beide Ablehnungen erfolgen sofort. FourA stellt den Request nicht in eine Warteschlange, um ihn später auszuführen. Es wird also nichts verbraucht und nichts berechnet. Abgelehnte Requests zählen jedoch für das gleitende Minutenfenster, weshalb ein Retry-Storm den eigenen Cooldown verlängert. Vollständige Feldreferenz: Rate Limits.
Zwei häufig übersehene Faktoren zählen mit:
- Ein
POST /api/auto/-Aufruf wird nicht als einzelner Request gezählt, aber jeder darin ausgeführte Single-, Proxy- und Browser-Sub-Call schon. Ein Auto-Call kann mehrere Slots belegen, während seine Ladder läuft. - Ein Browser-Request belegt seinen Slot so lange, wie das Rendern der Seite dauert. Das ist deutlich länger als bei einem Single-Request. Ein Batch von Browser-Aufrufen erreicht sein Limit daher mit weniger Requests als ein Batch von Single-Aufrufen.
Schritt 1: Begrenze deine eigene Concurrency
Wähle einen Wert unterhalb des Limits für den aufgerufenen Endpoint und halte ihn 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 als diese Anzahl an Calls offen, sodass der Batch eine Million URLs lang sein kann und trotzdem unter dem Limit bleibt.
Lass Puffer. Wenn zwei Prozesse auf deiner Seite einen API-Key teilen, teilen sie sich ein Limit, also gib jedem die Hälfte. Wenn ein schnelles Ziel dem Pool erlaubt, Requests schneller als in einer Minute abzuschließen, drossle die Worker, um auch unter dem Minutenlimit zu bleiben.
Schritt 2: Backoff statt direktem erneuten Senden
Wenn eine Ablehnung auftritt, ist es falsch, den Batch sofort erneut zu senden. Jeder darin enthaltene Call wird erneut abgelehnt, und die Retries stapeln sich auf den bereits laufenden Requests.
Warte zuerst. Die Wartezeit steht im Retry-After Header 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 Jitter wacht jeder Worker, der in derselben Sekunde abgelehnt wurde, in derselben Sekunde wieder auf und wird erneut gemeinsam abgelehnt.
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: Prüfe deinen tatsächlichen Verbrauch
Öffne Nutzung & Limits im Dashboard. Die Live-Zähler neben deinen Limits zeigen Nebenläufigkeit, Rate pro Minute und Browser-Requests pro Tag in Echtzeit an. So kannst du MAX_IN_FLIGHT anhand eines echten Durchlaufs dimensionieren statt nach Schätzungen.
Das Aktivitätsprotokoll erfasst auch Ablehnungen. Ein Durchlauf, der mit der richtigen Anzahl an Zeilen, aber vereinzelten rate_limit-Ergebnissen endete, hat sich selbst gedrosselt.
Häufige Fehler
- Die gesamte Liste auf einmal abfeuern.
asyncio.gatherüber 5.000 URLs oderPromise.allüber ein unbegrenztes Array öffnet 5.000 Aufrufe. Begrenze den Pool, nicht die Liste. - Parallele Wiederholungsversuche. Das erneute Senden jedes abgelehnten Aufrufs im Moment der Ablehnung erzeugt denselben Burst erneut, der zur Ablehnung geführt hat. Warte
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 Durchlauf und liesresets_ataus dem Body aus, falls vorhanden. - Auto-Aufrufe als jeweils einen Slot zählen. Die Kaskade innerhalb von
/api/auto/führt echte Unteraufrufe aus, und genau diese zählen für die Obergrenzen. - 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: Alle Ablehnungsarten und was jedes Feld bedeutet
- Response Headers:
X-FourA-LimitundRetry-After - Request-Ergebnisse: Warum ein
rate_limit-Ergebnis nie abgerechnet wird - Nutzung & Limits: Wo du die Live-Zähler und die Werte deines Plans findest
- Smart Fetch (Auto): Wie ein einzelner Auto-Aufruf in mehrere Unteraufrufe aufgeteilt wird