API-Tutorials

Batch-CAPTCHA-Lösung: Effizientes Senden mehrerer Aufgaben

Einen separaten Batch-Endpunkt gibt es bei CaptchaAI nicht: in.php nimmt genau eine Aufgabe pro Aufruf entgegen, parallelisiert wird auf Ihrer Seite. Genau daran entscheidet sich, ob ein Lauf Minuten oder Stunden dauert. Wer 200 Seiten crawlt, jede CAPTCHA-Abfrage einzeln einreicht und abwartet, addiert 200 Lösungszeiten hintereinander. Wer alle Aufgaben zuerst einreicht und die Ergebnisse anschließend gemeinsam abfragt, wartet im Wesentlichen nur einmal.

Dieser Artikel zeigt drei Muster dafür – Thread-Pool, asyncio und die strikte Trennung von Einreichung und Abfrage – und beantwortet die Frage, die in der Praxis vorher kommt: Wie viele Aufgaben dürfen überhaupt gleichzeitig laufen?


Wie viele Aufgaben parallel laufen dürfen: die Thread-Frage

CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung. Ein Thread ist eine Aufgabe, die gerade in Bearbeitung ist; sobald sie fertig ist, nimmt derselbe Thread die nächste. Die Zahl der Lösungen innerhalb des Abrechnungsmonats ist je Thread unbegrenzt.

Für die Batch-Planung heißt das: Die gebuchte Thread-Zahl ist die Obergrenze Ihrer sinnvollen Parallelität – und kein Budget, das sich je Aufgabe verbraucht.

  • BASIC (15 $/Monat, 5 Threads) – Entwicklung, Tests, kleine Nachtläufe
  • ADVANCE (90 $/Monat, 50 Threads) – regelmäßiges Web-Scraping im Produktivbetrieb
  • ENTERPRISE (300 $/Monat, 200 Threads) – verteilte Pipelines mit mehreren Workern

Preise in US-Dollar. Mehr max_workers zu setzen, als Threads gebucht sind, beschleunigt nichts: Die zusätzlichen Aufgaben warten dann lediglich an einer anderen Stelle.


Sequenziell oder parallel: der Zeitunterschied als Skizze

Der Unterschied ist kein Optimierungsdetail, sondern entscheidet darüber, ob ein Lauf noch in ein Wartungsfenster passt:

Sequential (slow):
  Submit #1 → Poll → Result (15s)
  Submit #2 → Poll → Result (15s)
  Submit #3 → Poll → Result (15s)
  Total: ~45s for 3 solves

Parallel (fast):
  Submit #1 ─┐
  Submit #2 ─┤→ Poll all → Results arrive
  Submit #3 ─┘
  Total: ~15s for 3 solves

Bei drei Aufgaben wirkt das unspektakulär. Bei 2.000 Cloudflare-Turnstile-Abfragen, die einzeln in unter 10 Sekunden gelöst sind, stehen gut fünf Stunden sequenziell gegen wenige Minuten mit 50 gleichzeitigen Aufgaben.


Variante 1: Thread-Pool für Batches bis rund 50 Aufgaben

Für die meisten Scraping-Jobs ist ThreadPoolExecutor die richtige Wahl: wenig Code, gut zu debuggen, und die Wartezeit auf die HTTP-Antwort ist ohnehin I/O-gebunden. Jede Aufgabe durchläuft ihren eigenen Zyklus aus Einreichen und Abfragen, as_completed liefert die Ergebnisse in der Reihenfolge, in der sie fertig werden.

import requests
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://ocr.captchaai.com"


def submit_task(method, **params):
    """Submit a single CAPTCHA task."""
    data = {"key": API_KEY, "method": method, "json": 1}
    data.update(params)
    resp = requests.post(f"{BASE_URL}/in.php", data=data, timeout=30)
    result = resp.json()
    if result.get("status") != 1:
        raise RuntimeError(f"Submit error: {result.get('request')}")
    return result["request"]


def poll_result(task_id, timeout=120):
    """Poll until result is ready."""
    start = time.time()
    while time.time() - start < timeout:
        time.sleep(5)
        resp = requests.get(f"{BASE_URL}/res.php", params={
            "key": API_KEY, "action": "get",
            "id": task_id, "json": 1,
        }, timeout=15)
        data = resp.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]
    raise TimeoutError(f"Task {task_id} timeout")


def solve_one(sitekey, pageurl):
    """Submit and poll a single task."""
    task_id = submit_task("userrecaptcha", googlekey=sitekey, pageurl=pageurl)
    token = poll_result(task_id)
    return {"url": pageurl, "token": token}


def batch_solve(tasks, max_workers=10):
    """Solve multiple CAPTCHAs in parallel."""
    results = []
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(solve_one, t["sitekey"], t["url"]): t
            for t in tasks
        }
        for future in as_completed(futures):
            task = futures[future]
            try:
                result = future.result()
                results.append(result)
                print(f"Solved: {result['url']}")
            except Exception as e:
                print(f"Failed: {task['url']} - {e}")
                results.append({"url": task["url"], "token": None, "error": str(e)})

    return results


# Usage
tasks = [
    {"sitekey": "SITE_KEY_1", "url": "https://example.com/page1"},
    {"sitekey": "SITE_KEY_2", "url": "https://example.com/page2"},
    {"sitekey": "SITE_KEY_3", "url": "https://example.com/page3"},
]

results = batch_solve(tasks, max_workers=5)
print(f"Solved {sum(1 for r in results if r.get('token'))}/{len(tasks)}")

Drei Details lohnen den zweiten Blick:

  • max_workers ist Ihre tatsächliche Parallelität – stimmen Sie den Wert auf die gebuchten Threads ab.
  • Fehlgeschlagene Aufgaben landen mit token: None im Ergebnis, statt den gesamten Lauf abzubrechen.
  • Jedes Ergebnis trägt seine URL mit; damit bleibt die Zuordnung eindeutig, auch wenn die Reihenfolge wechselt.

Variante 2: asyncio, wenn es Hunderte gleichzeitig werden

Oberhalb von etwa 100 gleichzeitigen Aufgaben wird ein Thread je Aufgabe teuer – jeder Thread belegt Speicher und erzwingt Kontextwechsel, obwohl er die meiste Zeit nur wartet. asyncio mit aiohttp erledigt dieselbe Arbeit in einem einzigen Prozess ohne zusätzliche Betriebssystem-Threads, und asyncio.Semaphore begrenzt die Parallelität exakt auf den Wert, den Ihr Thread-Kontingent hergibt.

import asyncio
import aiohttp
import time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://ocr.captchaai.com"


async def submit_task_async(session, method, **params):
    data = {"key": API_KEY, "method": method, "json": 1}
    data.update(params)
    async with session.post(f"{BASE_URL}/in.php", data=data) as resp:
        result = await resp.json()
        if result.get("status") != 1:
            raise RuntimeError(f"Submit error: {result.get('request')}")
        return result["request"]


async def poll_result_async(session, task_id, timeout=120):
    start = time.time()
    while time.time() - start < timeout:
        await asyncio.sleep(5)
        params = {
            "key": API_KEY, "action": "get",
            "id": task_id, "json": 1,
        }
        async with session.get(f"{BASE_URL}/res.php", params=params) as resp:
            data = await resp.json()
            if data["request"] != "CAPCHA_NOT_READY":
                return data["request"]
    raise TimeoutError(f"Task {task_id} timeout")


async def solve_one_async(session, sitekey, pageurl):
    task_id = await submit_task_async(
        session, "userrecaptcha",
        googlekey=sitekey, pageurl=pageurl,
    )
    token = await poll_result_async(session, task_id)
    return {"url": pageurl, "token": token}


async def batch_solve_async(tasks, max_concurrent=20):
    """Solve many CAPTCHAs concurrently with asyncio."""
    semaphore = asyncio.Semaphore(max_concurrent)
    results = []

    async def solve_with_limit(task):
        async with semaphore:
            try:
                result = await solve_one_async(
                    session, task["sitekey"], task["url"],
                )
                return result
            except Exception as e:
                return {"url": task["url"], "token": None, "error": str(e)}

    async with aiohttp.ClientSession() as session:
        coros = [solve_with_limit(t) for t in tasks]
        results = await asyncio.gather(*coros)

    return results


# Usage
tasks = [
    {"sitekey": "KEY", "url": f"https://example.com/page{i}"}
    for i in range(50)
]

results = asyncio.run(batch_solve_async(tasks, max_concurrent=20))
solved = sum(1 for r in results if r.get("token"))
print(f"Solved: {solved}/{len(tasks)}")

Der Semaphore-Wert ist dabei die eigentliche Stellschraube: Er hält die Zahl der offenen Anfragen konstant, während asyncio.gather die Ergebnisse einsammelt.


Variante 3: erst alles einreichen, dann gesammelt abfragen

Für maximalen Durchsatz trennen Sie die beiden Phasen. Zuerst gehen alle Aufgaben hinaus – mit rund 100 ms Abstand, damit die API nicht in einem Schlag getroffen wird. Danach fragt eine einzige Schleife sämtliche offenen Task-IDs ab und nimmt jedes Ergebnis heraus, sobald es vorliegt. Das spart je Aufgabe eine komplette Wartezeit, weil die ersten Lösungen bereits laufen, während Sie noch einreichen.

import requests
import time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://ocr.captchaai.com"


def batch_submit(tasks):
    """Submit all tasks first, return task IDs."""
    submitted = []
    for task in tasks:
        try:
            data = {
                "key": API_KEY,
                "method": "userrecaptcha",
                "googlekey": task["sitekey"],
                "pageurl": task["url"],
                "json": 1,
            }
            resp = requests.post(f"{BASE_URL}/in.php", data=data, timeout=30)
            result = resp.json()
            if result.get("status") == 1:
                submitted.append({
                    "task_id": result["request"],
                    "url": task["url"],
                })
            time.sleep(0.1)  # Brief delay between submits
        except Exception as e:
            print(f"Submit failed for {task['url']}: {e}")
    return submitted


def batch_poll(submitted, timeout=120):
    """Poll all submitted tasks until complete."""
    pending = {s["task_id"]: s for s in submitted}
    results = []
    start = time.time()

    while pending and time.time() - start < timeout:
        time.sleep(5)
        for task_id in list(pending.keys()):
            try:
                resp = requests.get(f"{BASE_URL}/res.php", params={
                    "key": API_KEY, "action": "get",
                    "id": task_id, "json": 1,
                }, timeout=15)
                data = resp.json()
                if data["request"] != "CAPCHA_NOT_READY":
                    info = pending.pop(task_id)
                    results.append({
                        "url": info["url"],
                        "token": data["request"],
                    })
            except Exception:
                pass

    # Mark remaining as failed
    for task_id, info in pending.items():
        results.append({"url": info["url"], "token": None, "error": "timeout"})

    return results


# Usage
tasks = [
    {"sitekey": "KEY", "url": f"https://example.com/page{i}"}
    for i in range(20)
]

submitted = batch_submit(tasks)
print(f"Submitted {len(submitted)} tasks")

results = batch_poll(submitted)
solved = sum(1 for r in results if r.get("token"))
print(f"Solved: {solved}/{len(tasks)}")

Entscheidend ist der Umgang mit Nachzüglern: Was nach dem Timeout noch in pending steht, wird als Fehlschlag markiert und geht in den nächsten Lauf – nicht in eine Endlosschleife.


Praxisbeispiel: nächtlicher Katalogabgleich im DACH-Raum

Ein Team betreibt einen Shopware-Shop und gleicht jede Nacht rund 800 Produktseiten autorisierter Lieferantenportale ab. Der Job läuft als GitLab-CI-Pipeline auf einem Hetzner-Cloud-Server; etwa jede vierte Seite zeigt eine reCAPTCHA-v2-Abfrage, also rund 200 Lösungen pro Nacht.

Sequenziell wären das bei bis zu 60 Sekunden je Lösung mehrere Stunden. Mit 50 gleichzeitigen Aufgaben und dem Submit-then-Poll-Muster passt derselbe Lauf in ein Wartungsfenster von etwa einer Stunde – an den Kosten ändert sich dabei nichts, weil Threads und nicht einzelne Lösungen abgerechnet werden.

Zwei Punkte, die in Projekten im deutschsprachigen Raum regelmäßig aufkommen: Klären Sie vorab, ob Sie die Zielseiten abrufen dürfen – eigene Systeme, Vertragspartner oder öffentlich zugängliche Daten –, und behandeln Sie IP-Adressen in Ihren Logs als personenbezogene Daten im Sinne der DSGVO. Ein Scraping-Log ist davon nicht ausgenommen.


Durchsatz realistisch planen

Gleichzeitige Aufgaben Ungefährer Durchsatz Typischer Einsatz
1–5 3–5 Lösungen/min Entwicklung, einzelne Testläufe
5–20 15–60 Lösungen/min laufendes Web-Scraping
20–50 60–150 Lösungen/min Pipelines mit hohem Volumen
50–100 150–300 Lösungen/min verteilte Worker, Unternehmensmaßstab

Die Werte sind Richtwerte für gemischte Aufgaben; die tatsächliche Rate hängt stark vom CAPTCHA-Typ ab. Ein Bild-CAPTCHA ist in unter 0,5 Sekunden gelöst, Cloudflare Turnstile in unter 10 Sekunden, reCAPTCHA v2 in unter 60 Sekunden – ein Batch ist immer so schnell wie sein langsamster Typ.


Was in Batches typischerweise schiefgeht

Symptom Ursache Gegenmaßnahme
HTTP 429 beim Einreichen zu viele Anfragen pro Sekunde 100 ms Pause zwischen den Übermittlungen einbauen
viele Timeouts Abfrage-Timeout zu kurz gewählt auf 120–180 Sekunden erhöhen
kaum Zuwachs oberhalb von 50 gleichzeitigen Aufgaben Netzwerk- oder Thread-Engpass auf asyncio (aiohttp) wechseln, Thread-Kontingent prüfen
Ergebnisse den falschen Seiten zugeordnet Task-ID nicht mitgeführt Ergebnisse in einem Dict mit task_id als Schlüssel halten
Token beim Absenden abgelehnt Ergebnis zu lange liegen geblieben Formular direkt nach Erhalt des Tokens absenden

Wie Sie Wiederholungen sauber staffeln, zeigt der Leitfaden zur Wiederholungslogik und Fehlerbehandlung in Node.js; die Grenzwerte selbst behandelt der Beitrag zu Rate-Limits und Anfragedrosselung.


Häufige Fragen

Brauche ich für Batch-Verarbeitung einen anderen Endpunkt?

Nein. Sie verwenden weiterhin in.php zum Einreichen und res.php zum Abfragen, nur eben mehrfach gleichzeitig. „Batch“ beschreibt Ihr Client-Muster, nicht eine eigene API.

Kann ich verschiedene CAPTCHA-Typen in einem Batch mischen?

Ja. Jede Aufgabe trägt ihre eigene method: userrecaptcha für reCAPTCHA v2 und v3, turnstile für Cloudflare Turnstile, geetest für GeeTest v3, post für Bild- und Rasterbild-CAPTCHAs. Planen Sie nur ein, dass die Typen unterschiedlich lange brauchen.

Wie vermeide ich 429-Antworten bei hoher Parallelität?

Verteilen Sie die Einreichungen, nicht die Aufgaben. Eine Pause von rund 100 ms zwischen den POST-Aufrufen genügt in der Regel; ausschlaggebend ist die Rate der Übermittlungen, nicht die Zahl der offenen Aufgaben.

Was passiert mit Tokens, die im Batch zu lange liegen bleiben?

Sie verfallen. reCAPTCHA-Tokens sind rund zwei Minuten gültig – lösen Sie deshalb erst dann, wenn das Formular auch abgesendet werden kann, statt Ergebnisse auf Vorrat zu halten.

Wie viele Threads brauche ich für 1.000 Lösungen pro Stunde?

Rechnen Sie mit der Lösungszeit Ihres häufigsten Typs. Cloudflare Turnstile ist in unter 10 Sekunden gelöst; ein Thread schafft damit rund 360 Lösungen pro Stunde, für 1.000 genügen drei bis vier Threads plus etwas Reserve. Bei reCAPTCHA v2 mit bis zu 60 Sekunden je Lösung sind es eher 17 bis 20 Threads.


Planen Sie Ihren nächsten Lauf nach Threads statt nach Lösungen – CaptchaAI testen und den ersten Batch parallel einreichen.

Kommentare sind für diesen Artikel deaktiviert.