Nebenläufigkeit, nicht Credits, entscheidet, wie lange dein Scraping-Job dauert
Derselbe 200.000-Request-Job braucht 22 Stunden bei 5 gleichzeitigen Slots, 4,4 Stunden bei 25. Warum Pläne die Nebenläufigkeit deckeln, und ein korrekter Worker-Pool in Python.

Zwei Accounts können exakt dasselbe Guthaben haben und denselben Job in völlig unterschiedlicher Zeit fertigstellen. Die Zahl, die entscheidet, wie lange ein Batch-Job läuft, ist die Nebenläufigkeit, also wie viele Requests du gleichzeitig offen haben darfst, und das ist meist nicht die Zahl, die man zuerst prüft.
Die Rechnung
Angenommen, ein Job besteht aus 200.000 Requests, und jeder braucht etwa 2 Sekunden Round-Trip, ein realistischer Durchschnitt für eine gerenderte Seite oder eine Suche mit Retries. Die Gesamtarbeit sind 400.000 Sekunden. Wie lange das dauert, hängt komplett davon ab, wie viele dieser Sekunden du parallel verbringen kannst:
| Gleichzeitige Slots | Gesamtzeit |
|---|---|
| 1 | 111,1 Stunden (4,6 Tage) |
| 5 | 22,2 Stunden |
| 10 | 11,1 Stunden |
| 25 | 4,4 Stunden |
Gleicher Job, gleiche Credit-Kosten, ein 25-facher Unterschied in der Wall-Clock-Zeit zwischen 1 Slot und 25. Wenn ein nächtlicher Job vor Beginn des Geschäftstags fertig sein muss, ist die Nebenläufigkeit die Zahl, die du prüfst, bevor du dich auf einen Zeitplan festlegst, nicht das Credit-Guthaben.
Warum Pläne die Nebenläufigkeit deckeln, nicht nur die Credits
Credits messen, wie viel Arbeit du insgesamt leisten darfst. Nebenläufigkeit misst, wie viel davon im selben Moment passieren kann, und existiert aus einem eigenen Grund: Jeder offene Request hält eine Browser-Session, eine Proxy-Verbindung oder beides, auf Infrastruktur, die Geld kostet, unabhängig davon, ob du nah an deinem Credit-Limit bist.
Bei FastAF skaliert die Nebenläufigkeit mit dem Credit-Paket, das du kaufst: Free läuft mit 2 gleichzeitigen Requests, jedes bezahlte Paket mit 5 (Starter, Growth, Scale und Pro), Business mit 25 (Stand Oktober 2026). Ein Free-Account kann zwei Requests gleichzeitig ausführen; ein Business-Account kann 25 gleichzeitig offen haben. Mehr Requests zu schicken, als dein Paket an Nebenläufigkeit erlaubt, reiht sich nicht brav ein und wird schneller, sondern liefert 429-Antworten. Das Limit ist es wert, den Job daran auszurichten, statt es durch Trial and Error zu entdecken.
Einen Worker-Pool schreiben, der das Limit respektiert
Der Fehler, der tatsächlich Zeit kostet, ist max_workers optimistisch zu setzen und die Retry-Logik die entstehenden 429s überkleben zu lassen. Setz es auf die tatsächliche Nebenläufigkeit deines Pakets, und die 429s hören größtenteils von vornherein auf.
ThreadPoolExecutor, die einfachere Option für die meisten Skripte:
import time
import requests
from concurrent.futures import ThreadPoolExecutor
API_KEY = "faf_live_..." # dein FastAF-Key
CONCURRENCY = 5 # passend zu deinem Paket: Free 2, Starter bis Pro 5, Business 25
def fetch(url, max_retries=5):
for attempt in range(max_retries):
resp = requests.get(
"https://api.fastaf.co/v1/scrape",
params={"url": url},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=90,
)
if resp.status_code == 200:
return resp.json()
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
continue
if resp.status_code in (502, 503, 504):
time.sleep(2 ** attempt) # vorübergehender Fehler beim Upstream, wird nicht abgerechnet
continue
resp.raise_for_status()
raise RuntimeError(f"gave up on {url} after {max_retries} attempts")
urls = [...] # deine 200.000 URLs
results = []
with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
for result in pool.map(fetch, urls):
results.append(result)
asyncio, wenn der Rest der Pipeline schon async ist, mit einem Semaphore, das denselben Job übernimmt, den oben max_workers gemacht hat:
import asyncio
import httpx
CONCURRENCY = 5
sem = asyncio.Semaphore(CONCURRENCY)
async def fetch(client, url, max_retries=5):
async with sem:
for attempt in range(max_retries):
resp = await client.get(
"https://api.fastaf.co/v1/scrape",
params={"url": url},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=90,
)
if resp.status_code == 200:
return resp.json()
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 2 ** attempt))
await asyncio.sleep(wait)
continue
if resp.status_code in (502, 503, 504):
await asyncio.sleep(2 ** attempt)
continue
resp.raise_for_status()
raise RuntimeError(f"gave up on {url} after {max_retries} attempts")
async def run(urls):
async with httpx.AsyncClient() as client:
return await asyncio.gather(*(fetch(client, u) for u in urls))
results = asyncio.run(run(urls))
Das Semaphore hält exakt dieselbe Nebenläufigkeitsgrenze wie max_workers oben; wähle den Stil, der zum Rest deiner Codebasis passt, nicht den einen aus Korrektheitsgründen.
Die zwei Dinge, die in der Praxis wirklich 429er verursachen
Mehr Worker als das Limit des Pakets erlaubt. Der naheliegende Fall: max_workers=50 mit einem Starter-Paket (5 gleichzeitig) bedeutet, dass 45 von 50 Requests sofort mit 429 scheitern, und die Backoff-Logik macht daraus nur langsam statt schnell.
Ein zweiter Prozess, den du vergessen hast. Ein Cron-Job und ein manueller Testlauf gegen denselben API-Key zur selben Zeit verdoppeln deine effektive Nebenläufigkeit, ohne dass einer der beiden Prozesse vom anderen weiß. Wenn du trotz korrekt gesetztem max_workers 429s siehst, prüf zuerst, ob ein zweites Skript gegen denselben Key läuft, bevor du annimmst, das Limit hätte sich geändert.
Kurz zusammengefasst
Bevor du schätzt, wie lange ein Batch-Job dauert, prüf die Nebenläufigkeit deines Pakets, nicht nur das Guthaben: 2 bei Free, 5 bei Starter, Growth, Scale und Pro, 25 bei Business (Stand Oktober 2026). Setz die Größe deines Worker-Pools auf diese Zahl, respektiere Retry-After bei 429 und mach exponentielles Backoff bei 5xx. Die Laufzeit eines Jobs ist (Gesamtanzahl Requests × durchschnittliche Latenz) / gleichzeitige Slots, und Nebenläufigkeit ist die einzige dieser drei Größen, die du durch ein größeres Paket tatsächlich ändern kannst.