Integrations

Selenium Grid + CaptchaAI: Distributed CAPTCHA Solving

Throughput otomatisasi Anda tidak ditentukan oleh jumlah browser, melainkan oleh dua kapasitas yang sering tertukar: berapa sesi browser yang boleh hidup bersamaan, dan berapa CAPTCHA yang boleh diselesaikan bersamaan. Selenium Grid mengatur angka pertama, paket thread CaptchaAI mengatur angka kedua. Begitu keduanya dihitung terpisah, keputusan "tambah node atau naikkan paket?" berhenti menjadi tebak-tebakan.

Artikel ini menyusun satu Grid Selenium 4 di Docker, memasang satu klien CaptchaAI yang dipakai seluruh node, menjalankan task secara paralel, lalu menaikkannya ke Kubernetes. Seluruh node berbagi satu API key yang sama — konkurensi dihitung di sisi API CaptchaAI, bukan per mesin.


Slot Grid dan thread CaptchaAI bukan hal yang sama

Satu slot Grid adalah satu sesi Chrome yang berjalan di sebuah node. Satu thread CaptchaAI adalah satu CAPTCHA yang sedang dikerjakan API. Sesi browser menghabiskan sebagian besar umurnya untuk navigasi dan mengisi form, hanya sepotong kecil untuk menunggu token — jadi kebutuhan thread Anda hampir selalu lebih kecil daripada jumlah slot Grid.

CaptchaAI menagih per thread, bukan per solve: setiap paket memberi solve tanpa batas selama bulan berjalan, tanpa biaya per CAPTCHA dan tanpa batas harian.

Paket Harga per bulan Thread
BASIC $15 5
STANDARD $30 15
ADVANCE $90 50
PREMIUM $170 100
CORPORATE $240 150
ENTERPRISE $300 200

Di atas itu ada VIP-1 ($1,500), VIP-2 ($4,500), dan VIP-3 ($7,500) dengan 1.000, 3.000, dan 5.000 thread.

Contoh untuk tim price-monitoring di Jakarta: 3 node x 5 sesi memberi 15 sesi paralel, satu task memakan sekitar dua menit, dan reCAPTCHA v2 selesai dalam waktu di bawah 60 detik. Berarti sekitar sepertiga sesi menunggu token pada saat bersamaan — 5–7 thread. STANDARD ($30 per bulan, 15 thread) sudah lapang, dan Anda naik paket saat node bertambah, bukan saat volume solve bertambah.


Peta jalur: hub, node, dan satu API key

Hub berperan sebagai router: klien Anda bicara ke satu alamat, hub memilih node dengan slot kosong. Tidak satu pun node menyimpan logika CAPTCHA sendiri — semuanya memanggil endpoint yang sama.

┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│  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)

Langkah 1: hidupkan hub dan tiga node Chrome

Docker Compose adalah cara paling praktis menyiapkan Grid di laptop maupun VPS. Tiga node dengan SE_NODE_MAX_SESSIONS=5 memberi 15 slot; siapkan sekitar 1 GB RAM per sesi Chrome agar node tidak dihentikan OOM killer.

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

Buka http://localhost:4444/ui dan pastikan ketiga node terdaftar sebelum Anda mengirim task pertama.


Langkah 2: bungkus API CaptchaAI menjadi satu klien bersama

Alurnya sama di semua node: kirim task ke in.php, simpan task ID, polling res.php sampai token siap, lalu suntikkan token ke halaman. Kelas di bawah menyatukan pembuatan sesi remote dan pemanggilan API dalam satu objek.

CaptchaAI menyelesaikan reCAPTCHA v2 dan v3 (termasuk varian Enterprise), Cloudflare Turnstile dan Cloudflare Challenge, GeeTest v3, CAPTCHA gambar/OCR, grid image, serta BLS. CaptchaFox (beta), Friendly Captcha (beta), dan Lemin (beta) tersedia dengan status beta. hCaptcha dan FunCaptcha (Arkose Labs) tidak didukung, sedangkan GeeTest v4 masih berstatus segera hadir — periksa tipe CAPTCHA di situs target sebelum Anda merancang Grid.

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("--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",
            "sitekey": 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()

Langkah 3: sebar task ke seluruh node

ThreadPoolExecutor menjaga berapa sesi yang hidup bersamaan. Aturannya sederhana: max_workers tidak boleh melebihi total slot Grid, dan sebaiknya tidak jauh melampaui jumlah thread paket Anda. Kalau worker melebihi slot, task menumpuk di antrean hub dan SessionNotCreated mulai bermunculan.

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")

Catat hasil per task, bukan hanya totalnya: saat satu node bermasalah, polanya terlihat lebih dulu di kolom error daripada di grafik CPU.


Langkah 4: baca kapasitas Grid sebelum menaikkan worker

Endpoint /status milik hub memberi tahu berapa slot yang benar-benar bebas. Panggil sekali di awal run dan pakai angkanya untuk menentukan jumlah worker, alih-alih konstanta yang hanya cocok di mesin Anda.

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}")

Langkah 5: naikkan ke Kubernetes dengan autoscaling

Kalau beban Anda bergelombang — sinkronisasi katalog tiap pagi lalu sepi sampai sore — jalankan node sebagai Deployment dan biarkan HorizontalPodAutoscaler menambah replika. Batas memori wajib diisi: node Chrome tanpa batas akan menggusur pod tetangga.

# 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

Tempatkan node di region yang sama dengan aplikasi Anda — ap-southeast-1 (Singapura), ap-southeast-3 (Jakarta), atau asia-southeast2 di GCP. Latensi ke API CaptchaAI kecil dibanding waktu penyelesaian CAPTCHA, jadi yang menentukan adalah jarak node ke situs target.


Klien Java untuk tim yang sudah berjalan di JVM

Tim QA yang memakai TestNG atau JUnit tidak perlu pindah ke Python: pola yang sama — sesi remote, executor dengan worker tetap, batas waktu per task — berlaku di Java.

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;
    }
}

Masalah yang paling sering muncul di Grid

Gejala Penyebab Penanganan
SessionNotCreated Tidak ada slot kosong Tambah node atau naikkan SE_NODE_MAX_SESSIONS
Timeout saat membuat sesi Node kelebihan beban Kurangi sesi bersamaan per node
WebDriverException Node terputus dari hub Tambahkan coba ulang untuk pembuatan sesi
Node dihentikan OOM Terlalu banyak instance browser Tetapkan batas memori dan sesi maksimal
Token tak kunjung siap Beban API sedang tinggi Perbesar jeda polling dan tambahkan percobaan ulang
Sesi menggantung Pembersihan sesi tertunda Setel SE_SESSION_TIMEOUT

Checklist sebelum dipakai di produksi

  • Simpan API key di environment variable, bukan di kode atau di image Docker.
  • Samakan max_workers dengan slot Grid yang tersedia dan dengan thread paket Anda.
  • Beri setiap task batas waktu total, bukan hanya batas waktu polling.
  • Pastikan driver.quit() selalu dipanggil di blok finally agar slot tidak bocor.
  • Ambil hanya data yang Anda berwenang memprosesnya; UU 27/2022 tentang Pelindungan Data Pribadi membuat batasan ini relevan untuk tim di Indonesia.

Pertanyaan yang sering muncul

Berapa thread CaptchaAI yang cocok untuk 20 node Grid?

Hitung dari waktu tunggu, bukan dari jumlah node: perkirakan bagian umur task yang dihabiskan menunggu token, lalu kalikan dengan jumlah sesi paralel. Untuk 20 node x 5 sesi dan sepertiga waktu menunggu, sekitar 35 thread masuk akal — ADVANCE ($90 per bulan, 50 thread) memberi ruang aman.

Bagaimana jika situs target memakai hCaptcha?

hCaptcha tidak didukung CaptchaAI, begitu pula FunCaptcha (Arkose Labs), sedangkan GeeTest v4 baru berstatus segera hadir. Untuk tipe tersebut Anda memerlukan layanan lain; arsitektur Grid-nya sendiri tetap bisa dipakai tanpa perubahan.

Solve berhasil, tetapi form tetap ditolak. Kenapa?

Umumnya token tidak sampai ke field yang benar atau sudah kedaluwarsa. Pastikan nilainya masuk ke g-recaptcha-response dan ke seluruh elemen bernama sama, lalu submit selagi token masih berlaku.

Lebih baik Grid sendiri di VPS atau di Kubernetes?

Sampai sekitar 20–30 sesi paralel, satu VPS dengan Docker Compose lebih murah dan lebih mudah dirawat. Pindah ke Kubernetes ketika satu mesin tidak lagi cukup untuk jam sibuk.

Perlukah tiap task memakai sesi baru?

Ya, demi isolasi: sesi baru berarti cookie dan state halaman bersih. Gunakan ulang sesi hanya bila serangkaian task berjalan di domain yang sama dan memang perlu berbagi state login.


Panduan terkait


Grid Anda sudah siap, tinggal tokennya — ambil API key CaptchaAI lalu jalankan solve pertama di node pertama.

Komentar dinonaktifkan untuk artikel ini.