Integrationen

Selenium Grid + CaptchaAI: Verteilte CAPTCHA-Lösung

Wenn ein einzelner Browser Ihre Test- oder Scraping-Pipeline ausbremst, ist verteiltes Lösen die Antwort: Selenium Grid verteilt Dutzende Browsersitzungen auf mehrere Knoten, und jede Sitzung löst ihr CAPTCHA unabhängig über dieselbe CaptchaAI-API. So kommen Sie von einer Handvoll Lösungen pro Minute auf mehrere Hundert – ohne Ihre Anwendungslogik anzufassen.

Dieser Leitfaden zeigt den kompletten Weg: den Grid mit Selenium 4 aufsetzen, die Chrome-Nodes konfigurieren, CaptchaAI in den Client einbinden und den Durchsatz sauber skalieren. Behandelt werden reCAPTCHA v2 und Cloudflare Turnstile, dazu die Frage, wie viele Threads Sie für eine bestimmte Knotenzahl tatsächlich brauchen.


Architektur: Hub, Nodes und eine gemeinsame CaptchaAI-API

Ein Selenium Grid besteht aus einem Hub, der als Router arbeitet, und beliebig vielen Nodes, die jeweils mehrere Chrome-Instanzen betreiben. Ihr Testskript spricht ausschließlich mit dem Hub; dieser verteilt jede neue Sitzung auf einen freien Slot. Entscheidend für die CAPTCHA-Automatisierung: Alle Nodes teilen sich denselben CaptchaAI-API-Schlüssel. Es gibt keine zentrale Solve-Instanz, die zum Flaschenhals würde – jede Browsersitzung schickt ihre eigene Anfrage an die API und wartet auf ihr eigenes Ergebnis.

┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│  Test Script │────▶│  Grid Hub    │────▶│  Node 1      │
│  (Client)    │     │  (Router)    │     │  Chrome x 5  │
└─────────────┘     └──────────────┘     └──────────────┘
                           │              ┌──────────────┐
                           ├─────────────▶│  Node 2      │
                           │              │  Chrome x 5  │
                           │              └──────────────┘
                           │              ┌──────────────┐
                           └─────────────▶│  Node 3      │
                                          │  Chrome x 5  │
                                          └──────────────┘

All nodes share ──▶ CaptchaAI API (single API key)

Selenium Grid 4 mit Docker Compose aufsetzen

Der schnellste Weg zu einem lauffähigen Grid ist Docker Compose: ein Hub-Container plus drei Node-Container, die sich über den Event-Bus beim Hub registrieren. Über SE_NODE_MAX_SESSIONS legen Sie fest, wie viele Chrome-Sitzungen ein Node gleichzeitig fährt – hier fünf pro Node, also 15 parallele Sitzungen insgesamt.

Hub und drei Chrome-Nodes per Docker Compose

version: "3"
services:
  selenium-hub:
    image: selenium/hub:4.21.0
    container_name: selenium-hub
    ports:

      - "4442:4442"
      - "4443:4443"
      - "4444:4444"

  chrome-node-1:
    image: selenium/node-chrome:4.21.0
    depends_on:

      - selenium-hub
    environment:

      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=5
      - SE_NODE_OVERRIDE_MAX_SESSIONS=true

  chrome-node-2:
    image: selenium/node-chrome:4.21.0
    depends_on:

      - selenium-hub
    environment:

      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=5
      - SE_NODE_OVERRIDE_MAX_SESSIONS=true

  chrome-node-3:
    image: selenium/node-chrome:4.21.0
    depends_on:

      - selenium-hub
    environment:

      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=5
      - SE_NODE_OVERRIDE_MAX_SESSIONS=true
docker-compose up -d

Nach dem Start erreichen Sie die Grid-Konsole unter http://localhost:4444 und sehen dort alle registrierten Nodes samt freier Slots.


Der CaptchaAI-Client für jeden Grid-Knoten

Die Klasse GridCaptchaSolver kapselt zwei Dinge: das Erzeugen einer Remote-Sitzung auf dem Grid und das Lösen des CAPTCHAs über CaptchaAI. Der Ablauf pro CAPTCHA ist bei jedem unterstützten Typ gleich – Aufgabe an in.php übermitteln, anschließend das Ergebnis an res.php abfragen (Polling), bis das Token zurückkommt. Für reCAPTCHA v2 landet der zurückgegebene Wert im Feld g-recaptcha-response, das per JavaScript in die Seite eingetragen wird; für Turnstile funktioniert das Muster analog.

import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from concurrent.futures import ThreadPoolExecutor, as_completed


class GridCaptchaSolver:
    CAPTCHAAI_URL = "https://ocr.captchaai.com"

    def __init__(self, api_key, grid_url="http://localhost:4444"):
        self.api_key = api_key
        self.grid_url = grid_url

    def create_session(self):
        """Create a new browser session on the Grid."""
        options = webdriver.ChromeOptions()
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-blink-features=AutomationControlled")
        options.add_argument("--window-size=1920,1080")

        driver = webdriver.Remote(
            command_executor=self.grid_url,
            options=options,
        )
        return driver

    def solve_recaptcha_v2(self, site_url, sitekey):
        """Solve reCAPTCHA v2 via CaptchaAI API."""
        # Submit
        resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": site_url,
            "json": 1,
        })
        data = resp.json()
        if data["status"] != 1:
            raise Exception(f"Submit: {data['request']}")

        task_id = data["request"]

        # Poll
        for _ in range(60):
            time.sleep(5)
            resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
                "key": self.api_key, "action": "get",
                "id": task_id, "json": 1,
            })
            data = resp.json()
            if data["request"] == "CAPCHA_NOT_READY":
                continue
            if data["status"] != 1:
                raise Exception(f"Solve: {data['request']}")
            return data["request"]

        raise Exception("Timeout")

    def solve_turnstile(self, site_url, sitekey):
        resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
            "key": self.api_key, "method": "turnstile",
            "key": sitekey, "pageurl": site_url, "json": 1,
        })
        data = resp.json()
        if data["status"] != 1:
            raise Exception(f"Submit: {data['request']}")

        task_id = data["request"]
        for _ in range(60):
            time.sleep(5)
            resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
                "key": self.api_key, "action": "get",
                "id": task_id, "json": 1,
            })
            data = resp.json()
            if data["request"] == "CAPCHA_NOT_READY":
                continue
            if data["status"] != 1:
                raise Exception(f"Solve: {data['request']}")
            return data["request"]

        raise Exception("Timeout")

    def process_task(self, task):
        """Process a single CAPTCHA-protected task on a Grid node."""
        driver = self.create_session()

        try:
            driver.get(task["url"])
            time.sleep(2)

            # Detect sitekey
            sitekey = task.get("sitekey")
            if not sitekey:
                sitekey = driver.execute_script(
                    "return document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')"
                )

            if not sitekey:
                return {"url": task["url"], "status": "no_captcha", "data": driver.page_source[:500]}

            # Solve
            token = self.solve_recaptcha_v2(task["url"], sitekey)

            # Inject
            driver.execute_script(f"""
                document.querySelector('#g-recaptcha-response').value = '{token}';
                document.querySelectorAll('[name="g-recaptcha-response"]').forEach(
                    el => el.value = '{token}'
                );
            """)

            # Fill form and submit
            if task.get("form_data"):
                for field, value in task["form_data"].items():
                    driver.find_element(By.NAME, field).send_keys(value)

            if task.get("submit_selector"):
                driver.find_element(By.CSS_SELECTOR, task["submit_selector"]).click()
                time.sleep(3)

            return {
                "url": task["url"],
                "status": "success",
                "result_url": driver.current_url,
                "data": driver.page_source[:1000],
            }

        except Exception as e:
            return {"url": task["url"], "status": "error", "error": str(e)}

        finally:
            driver.quit()

Wichtig ist der finally-Block mit driver.quit(): Jede Sitzung wird nach der Aufgabe sauber geschlossen, damit der Slot sofort wieder frei ist. Verwaiste Sitzungen sind die häufigste Ursache dafür, dass ein Grid unter Last „vollläuft".


CAPTCHA-Aufgaben parallel über den Grid verteilen

Ein ThreadPoolExecutor reicht aus, um viele Aufgaben gleichzeitig loszuschicken. Jeder Worker greift sich eine Aufgabe, öffnet eine eigene Grid-Sitzung, löst das CAPTCHA und liefert ein Ergebnisobjekt zurück. Über max_workers steuern Sie die Parallelität – setzen Sie den Wert nie höher als die Zahl freier Grid-Slots, sonst warten Worker nur auf eine Sitzung.

def run_parallel_tasks(api_key, tasks, max_workers=10):
    """Run CAPTCHA tasks in parallel across Grid nodes."""
    solver = GridCaptchaSolver(api_key)
    results = []

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(solver.process_task, task): task
            for task in tasks
        }

        for future in as_completed(futures):
            task = futures[future]
            try:
                result = future.result(timeout=600)
                results.append(result)
                print(f"[{result['status']}] {result['url']}")
            except Exception as e:
                results.append({
                    "url": task["url"],
                    "status": "exception",
                    "error": str(e),
                })

    return results


# Usage
tasks = [
    {
        "url": "https://site-a.com/form",
        "sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
        "form_data": {"name": "Test User", "email": "test@example.com"},
        "submit_selector": "#submit",
    },
    {
        "url": "https://site-b.com/register",
        "sitekey": "6LdKlZEpAAAAAAOQjzC2v_mJ-",
        "form_data": {"username": "testuser"},
        "submit_selector": "button[type='submit']",
    },
    # Add more tasks...
]

results = run_parallel_tasks("YOUR_API_KEY", tasks, max_workers=15)

# Summary
success = sum(1 for r in results if r["status"] == "success")
print(f"\nCompleted: {success}/{len(results)} successful")

Threads, Pläne und Kosten beim verteilten Lösen

Die entscheidende Planungsgröße ist nicht die Knotenzahl, sondern die Zahl der gleichzeitig laufenden CAPTCHAs. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – innerhalb eines Plans sind die Lösungen unbegrenzt. Ein Thread entspricht einem CAPTCHA, das gerade in Bearbeitung ist; sobald es gelöst ist, nimmt derselbe Thread das nächste.

Rechnen Sie es an Ihrem Grid durch: Zehn Knoten zu je fünf Sitzungen fahren 50 Browser gleichzeitig. Löst in dem Moment jeder Browser ein CAPTCHA, brauchen Sie 50 parallele Threads – das deckt der Plan ADVANCE (90 $/Monat, 50 Threads) ab. Wer auf 100 parallele Sitzungen skaliert, greift zu PREMIUM (170 $/Monat, 100 Threads), für kleinere Test-Grids reicht STANDARD (30 $/Monat, 15 Threads). Preise verstehen sich in US-Dollar. Weil nicht jede Sitzung ununterbrochen ein CAPTCHA löst, liegt der reale Thread-Bedarf meist unter der theoretischen Sitzungszahl – messen Sie mit einem kleinen Grid und skalieren Sie den Plan dann gezielt nach.


Grid-Auslastung überwachen und Worker anpassen

Bevor Sie eine große Warteschlange abarbeiten, lohnt ein Blick auf die freie Kapazität des Grids. Der Status-Endpunkt liefert für jeden Node die belegten und freien Slots – daraus leiten Sie die sinnvolle Worker-Zahl direkt ab, statt sie zu raten.

import requests

def check_grid_status(grid_url="http://localhost:4444"):
    """Check Selenium Grid status and available nodes."""
    try:
        resp = requests.get(f"{grid_url}/status")
        data = resp.json()

        nodes = data.get("value", {}).get("nodes", [])
        total_slots = 0
        available_slots = 0

        print(f"Grid Status: {data['value']['ready']}")
        print(f"Nodes: {len(nodes)}")

        for i, node in enumerate(nodes):
            slots = node.get("slots", [])
            free = sum(1 for s in slots if not s.get("session"))
            total_slots += len(slots)
            available_slots += free
            print(f"  Node {i+1}: {free}/{len(slots)} slots available")

        print(f"Total capacity: {available_slots}/{total_slots} available")
        return available_slots

    except Exception as e:
        print(f"Grid check failed: {e}")
        return 0


# Adjust workers based on grid capacity
available = check_grid_status()
optimal_workers = min(available, 20)
print(f"Optimal workers: {optimal_workers}")

Nodes automatisch skalieren mit Kubernetes

Wenn Ihre Last schwankt, ist ein statisches Docker-Compose-Grid nicht mehr die beste Wahl. Mit Kubernetes betreiben Sie die Chrome-Nodes als Deployment und lassen einen Horizontal Pod Autoscaler die Replikazahl anhand der CPU-Auslastung anpassen – hier zwischen 2 und 20 Pods, mit einem Ziel von 70 % Auslastung. In DACH-Umgebungen laufen solche Cluster häufig auf Hetzner, IONOS oder netcup; die Konfiguration bleibt identisch, egal ob Sie in Nürnberg oder in einer Public Cloud deployen.

# selenium-grid-k8s.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: selenium-chrome-node
spec:
  replicas: 5
  selector:
    matchLabels:
      app: selenium-chrome
  template:
    metadata:
      labels:
        app: selenium-chrome
    spec:
      containers:

        - name: chrome
          image: selenium/node-chrome:4.21.0
          env:

            - name: SE_EVENT_BUS_HOST
              value: selenium-hub

            - name: SE_EVENT_BUS_PUBLISH_PORT
              value: "4442"

            - name: SE_EVENT_BUS_SUBSCRIBE_PORT
              value: "4443"

            - name: SE_NODE_MAX_SESSIONS
              value: "3"
          resources:
            limits:
              memory: "2Gi"
              cpu: "1"
            requests:
              memory: "1Gi"
              cpu: "500m"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: chrome-node-hpa
spec:
  scaleRef:
    apiVersion: apps/v1
    kind: Deployment
    name: selenium-chrome-node
  minReplicas: 2
  maxReplicas: 20
  metrics:

    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Achten Sie darauf, dass Ihr CaptchaAI-Plan mit der maximalen Node-Zahl mitwächst: 20 Pods zu je 3 Sitzungen bedeuten bis zu 60 gleichzeitige Threads in der Spitze.


Grid-Anbindung in Java

Nicht jedes Team arbeitet in Python. Für JVM-basierte Test-Stacks – in vielen deutschen Enterprise-Pipelines der Standard – lässt sich derselbe Ansatz mit RemoteWebDriver und einem ExecutorService umsetzen. Die Grid-URL und der API-Schlüssel werden injiziert, die parallele Ausführung übernimmt ein fester Thread-Pool.

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.net.URL;
import java.net.http.*;
import java.net.URI;
import java.util.concurrent.*;

public class GridCaptchaSolver {
    private final String apiKey;
    private final String gridUrl;
    private final HttpClient httpClient;

    public GridCaptchaSolver(String apiKey, String gridUrl) {
        this.apiKey = apiKey;
        this.gridUrl = gridUrl;
        this.httpClient = HttpClient.newHttpClient();
    }

    public WebDriver createSession() throws Exception {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--no-sandbox", "--window-size=1920,1080");
        return new RemoteWebDriver(new URL(gridUrl), options);
    }

    public List<Map<String, String>> runParallel(
        List<Map<String, String>> tasks, int workers
    ) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(workers);
        List<Future<Map<String, String>>> futures = new ArrayList<>();

        for (Map<String, String> task : tasks) {
            futures.add(executor.submit(() -> processTask(task)));
        }

        List<Map<String, String>> results = new ArrayList<>();
        for (Future<Map<String, String>> future : futures) {
            results.add(future.get(600, TimeUnit.SECONDS));
        }

        executor.shutdown();
        return results;
    }
}

Häufige Grid-Probleme beheben

Die meisten Störungen im verteilten Betrieb sind Kapazitäts- oder Ressourcenprobleme, keine CAPTCHA-Probleme. Diese Tabelle ordnet Symptom, Ursache und Gegenmaßnahme zu:

Problem Ursache Lösung
SessionNotCreated Keine verfügbaren Slots Knotenanzahl oder SE_NODE_MAX_SESSIONS erhöhen
Timeout im Grid Knoten überlastet Gleichzeitige Sitzungen pro Knoten reduzieren
WebDriverException Knoten getrennt Wiederholungslogik für die Sitzungserstellung ergänzen
Speicher erschöpft Zu viele Browserinstanzen Ressourcenlimits und maximale Sitzungen festlegen
Timeout beim CAPTCHA-Lösen API unter Last Polling-Timeout erhöhen und erneute Versuche einbauen
Veraltete Sitzungen Verzögerung bei der Grid-Bereinigung SE_SESSION_TIMEOUT setzen

FAQ

Wie viele Threads brauche ich für meinen Grid?

So viele, wie im Spitzenmoment CAPTCHAs gleichzeitig in Bearbeitung sind. Ein Grid mit 50 parallelen Sitzungen deckt der Plan ADVANCE (90 $/Monat, 50 Threads) ab, 100 parallele Sitzungen PREMIUM (170 $/Monat, 100 Threads). Da nicht jede Sitzung ständig ein CAPTCHA löst, liegt der reale Bedarf meist niedriger – erst messen, dann den Plan wählen.

Was passiert, wenn ein Token abläuft, bevor das Formular abgesendet wird?

Dann wird es von der Zielseite abgelehnt. reCAPTCHA- und Turnstile-Tokens leben nur rund 120 Sekunden. Lösen Sie das CAPTCHA deshalb erst unmittelbar vor dem Absenden und tragen Sie das Token direkt danach in das Formularfeld ein – nicht Minuten im Voraus auf Vorrat.

Kann ich den Grid auf Hetzner oder eigener Infrastruktur betreiben?

Ja. Das Docker-Compose- wie auch das Kubernetes-Setup laufen unverändert auf Hetzner, IONOS, netcup oder jeder anderen Cloud. CaptchaAI wird über HTTPS angesprochen und ist unabhängig davon, wo Ihre Nodes stehen – nur die ausgehende Verbindung zur API muss offen sein.

Neue Sitzung pro Aufgabe oder Sitzungen wiederverwenden?

Für saubere Isolierung erstellen Sie pro Aufgabe eine neue Sitzung. Wiederverwenden lohnt sich nur, wenn mehrere Aufgaben dieselbe Domain und dieselben Cookies nutzen – dann sparen Sie den Overhead des Sitzungsaufbaus.


Verwandte Leitfäden


Skalieren Sie das CAPTCHA-Lösen über verteilte Browserinstanzen – Holen Sie sich Ihren CaptchaAI-Schlüssel und rollen Sie es mit Selenium Grid aus.

Kommentare sind für diesen Artikel deaktiviert.