Tutorials

Testando CaptchaAI antes da migração completa: guia de execução paralela

Três dias de execução paralela valem mais do que qualquer tabela comparativa. Você mantém o provedor atual em produção, manda o mesmo desafio também para a CaptchaAI e termina a semana com três números que são seus: taxa de resolução, tempo até o token e quantas resoluções ficam em voo no pico.

O que a execução paralela responde

Uma pergunta bem específica: como os dois provedores se comportam com o seu tráfego, no mesmo instante e na mesma máquina. Nenhuma página de preços sabe quais sitekeys aparecem no seu funil nem em que horário o pico acontece. Um worker em sa-east-1 (São Paulo) mede um RTT bem diferente de um script no notebook do desenvolvedor.

As cinco métricas que decidem a troca

Defina o que vai medir antes da primeira linha de código. Taxa de sucesso sozinha engana: um provedor pode acertar quase tudo e ainda estourar o seu tempo limite no pico.

Métrica Como medir
Taxa de resolução sucessos / tentativas × 100
Tempo médio Do envio em in.php até o token em res.php
Tempo P95 95º percentil — a cauda que a média esconde
Erros por código Separando falha de resolução de falha de integração
Validade do token O token passa na verificação do backend?

Grave cada tentativa com o rótulo do provedor. Sem esse registro, a comparação vira impressão.

Como o roteador divide os dois lados

Na comparação, os dois provedores recebem o mesmo desafio; depois, fatias crescentes vão só para o desafiante. Como a API da CaptchaAI segue o padrão in.php / res.php, uma única classe atende aos dois lados.

                    ┌──────────────┐
                    │ Your App     │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │ CAPTCHA      │
                    │ Router       │
                    └──┬───────┬───┘
                       │       │
              ┌────────▼──┐ ┌──▼────────┐
              │ Current   │ │ CaptchaAI │
              │ Provider  │ │           │
              └────────┬──┘ └──┬────────┘
                       │       │
                    ┌──▼───────▼──┐
                    │ Metrics     │
                    │ Collector   │
                    └─────────────┘

Passo 1: uma classe de provedor para os dois lados

A classe envia a tarefa com method=userrecaptcha, guarda o captcha_id e consulta res.php até o token chegar ou o limite estourar. O campo elapsed é cronometrado do envio até a resposta final — é ele que alimenta a média e o P95.

import os
import time
import requests
from dataclasses import dataclass, field
from typing import Optional
from concurrent.futures import ThreadPoolExecutor


@dataclass
class SolveResult:
    provider: str
    success: bool
    solution: Optional[str] = None
    error: Optional[str] = None
    elapsed: float = 0.0
    cost: float = 0.0


class CaptchaProvider:
    def __init__(self, name, submit_url, result_url, api_key):
        self.name = name
        self.submit_url = submit_url
        self.result_url = result_url
        self.api_key = api_key
        self.session = requests.Session()

    def solve_recaptcha(self, sitekey, pageurl):
        start = time.time()

        resp = self.session.post(self.submit_url, data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": 1
        })
        data = resp.json()
        if data.get("status") != 1:
            return SolveResult(
                provider=self.name, success=False,
                error=data.get("request"), elapsed=time.time() - start
            )

        captcha_id = data["request"]

        for _ in range(60):
            time.sleep(5)
            result = self.session.get(self.result_url, params={
                "key": self.api_key, "action": "get",
                "id": captcha_id, "json": 1
            }).json()

            if result.get("status") == 1:
                return SolveResult(
                    provider=self.name, success=True,
                    solution=result["request"], elapsed=time.time() - start
                )
            if result.get("request") != "CAPCHA_NOT_READY":
                return SolveResult(
                    provider=self.name, success=False,
                    error=result.get("request"), elapsed=time.time() - start
                )

        return SolveResult(
            provider=self.name, success=False,
            error="TIMEOUT", elapsed=time.time() - start
        )

Nada aí é específico da CaptchaAI: a mesma classe atende o provedor antigo, então você compara os dois serviços e não duas implementações de polling diferentes.

Passo 2: disparar as duas resoluções no mesmo instante

O ThreadPoolExecutor com dois workers põe os provedores na mesma janela de rede. Resolvendo primeiro em um e depois no outro, o segundo mediria outro minuto do dia.

class ParallelTestRunner:
    def __init__(self, primary, challenger):
        self.primary = primary
        self.challenger = challenger
        self.results = {"primary": [], "challenger": []}

    def run_test(self, sitekey, pageurl, num_runs=20):
        print(f"Running {num_runs} parallel solves...")

        for i in range(num_runs):
            with ThreadPoolExecutor(max_workers=2) as executor:
                primary_future = executor.submit(
                    self.primary.solve_recaptcha, sitekey, pageurl
                )
                challenger_future = executor.submit(
                    self.challenger.solve_recaptcha, sitekey, pageurl
                )

                primary_result = primary_future.result()
                challenger_result = challenger_future.result()

            self.results["primary"].append(primary_result)
            self.results["challenger"].append(challenger_result)

            print(f"  Run {i+1}/{num_runs}: "
                  f"{self.primary.name}={'OK' if primary_result.success else 'FAIL'} "
                  f"({primary_result.elapsed:.1f}s) | "
                  f"{self.challenger.name}={'OK' if challenger_result.success else 'FAIL'} "
                  f"({challenger_result.elapsed:.1f}s)")

        return self.generate_report()

    def generate_report(self):
        report = {}
        for label, results in self.results.items():
            total = len(results)
            successes = sum(1 for r in results if r.success)
            times = [r.elapsed for r in results if r.success]
            errors = [r.error for r in results if not r.success]

            report[label] = {
                "provider": results[0].provider if results else "unknown",
                "total": total,
                "successes": successes,
                "success_rate": (successes / total * 100) if total else 0,
                "avg_time": sum(times) / len(times) if times else 0,
                "min_time": min(times) if times else 0,
                "max_time": max(times) if times else 0,
                "errors": errors
            }

        return report


# Usage
current = CaptchaProvider(
    name="CurrentProvider",
    submit_url="https://current-provider.com/in.php",
    result_url="https://current-provider.com/res.php",
    api_key="current_key"
)

captchaai = CaptchaProvider(
    name="CaptchaAI",
    submit_url="https://ocr.captchaai.com/in.php",
    result_url="https://ocr.captchaai.com/res.php",
    api_key=os.environ["CAPTCHAAI_API_KEY"]
)

runner = ParallelTestRunner(primary=current, challenger=captchaai)
report = runner.run_test(
    sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    pageurl="https://example.com/form",
    num_runs=20
)

for label, stats in report.items():
    print(f"\n{stats['provider']}:")
    print(f"  Success rate: {stats['success_rate']:.1f}%")
    print(f"  Avg time: {stats['avg_time']:.1f}s")
    print(f"  Min/Max: {stats['min_time']:.1f}s / {stats['max_time']:.1f}s")
    if stats['errors']:
        print(f"  Errors: {stats['errors']}")

Salve o relatório em JSON a cada rodada: terça de manhã e quinta às 20h (horário de Brasília) rendem números bem diferentes.

Passo 3: mandar uma fatia do tráfego real

Com a comparação fechada, mande uma porcentagem pequena do tráfego de produção ao desafiante, com retorno ao provedor atual em caso de falha.

import random


class TrafficSplitter:
    def __init__(self, primary, challenger, challenger_pct=10):
        self.primary = primary
        self.challenger = challenger
        self.challenger_pct = challenger_pct

    def solve(self, sitekey, pageurl):
        if random.randint(1, 100) <= self.challenger_pct:
            result = self.challenger.solve_recaptcha(sitekey, pageurl)
            if not result.success:
                # Fall back to primary on failure
                return self.primary.solve_recaptcha(sitekey, pageurl)
            return result
        return self.primary.solve_recaptcha(sitekey, pageurl)


# Start with 10%, increase as confidence builds
splitter = TrafficSplitter(current, captchaai, challenger_pct=10)
result = splitter.solve(sitekey="...", pageurl="...")

Comece em 10% e suba um degrau a cada 24 horas estáveis. O fallback protege o usuário final enquanto você coleta evidência.

O mesmo teste em Node.js com Promise.all

Em Node.js a lógica é idêntica — mesma submissão, mesmo polling, mesmo registro de tempo — com Promise.all no lugar do pool de threads.

const axios = require("axios");

class CaptchaProvider {
  constructor(name, submitUrl, resultUrl, apiKey) {
    this.name = name;
    this.submitUrl = submitUrl;
    this.resultUrl = resultUrl;
    this.apiKey = apiKey;
  }

  async solveRecaptcha(sitekey, pageurl) {
    const start = Date.now();
    try {
      const submit = await axios.post(this.submitUrl, null, {
        params: { key: this.apiKey, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
      });
      if (submit.data.status !== 1) {
        return { provider: this.name, success: false, error: submit.data.request, elapsed: (Date.now() - start) / 1000 };
      }

      const captchaId = submit.data.request;
      for (let i = 0; i < 60; i++) {
        await new Promise((r) => setTimeout(r, 5000));
        const poll = await axios.get(this.resultUrl, {
          params: { key: this.apiKey, action: "get", id: captchaId, json: 1 },
        });
        if (poll.data.status === 1) {
          return { provider: this.name, success: true, solution: poll.data.request, elapsed: (Date.now() - start) / 1000 };
        }
        if (poll.data.request !== "CAPCHA_NOT_READY") {
          return { provider: this.name, success: false, error: poll.data.request, elapsed: (Date.now() - start) / 1000 };
        }
      }
      return { provider: this.name, success: false, error: "TIMEOUT", elapsed: (Date.now() - start) / 1000 };
    } catch (err) {
      return { provider: this.name, success: false, error: err.message, elapsed: (Date.now() - start) / 1000 };
    }
  }
}

async function parallelTest(current, captchaai, sitekey, pageurl, runs = 20) {
  const results = { current: [], captchaai: [] };

  for (let i = 0; i < runs; i++) {
    const [currentResult, captchaaiResult] = await Promise.all([
      current.solveRecaptcha(sitekey, pageurl),
      captchaai.solveRecaptcha(sitekey, pageurl),
    ]);

    results.current.push(currentResult);
    results.captchaai.push(captchaaiResult);

    console.log(`Run ${i + 1}/${runs}: ${current.name}=${currentResult.success ? "OK" : "FAIL"} ` +
      `(${currentResult.elapsed.toFixed(1)}s) | ${captchaai.name}=${captchaaiResult.success ? "OK" : "FAIL"} ` +
      `(${captchaaiResult.elapsed.toFixed(1)}s)`);
  }

  for (const [label, data] of Object.entries(results)) {
    const successes = data.filter((r) => r.success).length;
    const times = data.filter((r) => r.success).map((r) => r.elapsed);
    const avgTime = times.length ? times.reduce((a, b) => a + b, 0) / times.length : 0;
    console.log(`\n${label}: ${successes}/${runs} success (${((successes / runs) * 100).toFixed(1)}%), avg ${avgTime.toFixed(1)}s`);
  }
}

// Run
const currentProvider = new CaptchaProvider("CurrentProvider", "https://current-provider.com/in.php", "https://current-provider.com/res.php", "current_key");
const captchaai = new CaptchaProvider("CaptchaAI", "https://ocr.captchaai.com/in.php", "https://ocr.captchaai.com/res.php", process.env.CAPTCHAAI_API_KEY);

parallelTest(currentProvider, captchaai, "SITE_KEY", "https://example.com", 20);

Custo: pense em threads, não em resoluções

A CaptchaAI cobra por thread concorrente, com resoluções ilimitadas por thread no mês. A pergunta do teste, portanto, não é quanto sai cada resolução, e sim quantas ficam em voo ao mesmo tempo no seu pico.

Uma equipe de QA em São Paulo que roda a suíte de formulários à noite chega a 12 resoluções simultâneas: cabe no STANDARD (US$ 30/mês, 15 threads). Se o pico subir para 40, o ADVANCE (US$ 90/mês, 50 threads) dá conta. A escala vai do BASIC (US$ 15/mês, 5 threads) ao VIP-3 (US$ 7.500/mês, 5.000 threads).

Confira também a cobertura de tipos: reCAPTCHA v2 e v3 (inclusive Enterprise), Cloudflare Turnstile e Challenge, GeeTest v3, imagem/OCR, grade de imagens e BLS, mais CaptchaFox (beta), Friendly Captcha (beta) e Lemin (beta). hCaptcha e FunCaptcha (Arkose Labs) não são suportados; o GeeTest v4 consta como "em breve".

Quatro fases da execução paralela até o corte

Fase Duração Tráfego Objetivo
1. Validação 1 dia Só paralelo Confirmar API e token
2. Sombra 3 dias 5% com fallback Linha de base em tráfego real
3. Rampa 1 semana 25% → 50% → 75% Taxa e P95 em cada degrau
4. Corte Tudo na CaptchaAI Desativar o provedor antigo

Mantenha a chave do provedor antigo ativa por mais um ciclo: voltar atrás com uma variável de ambiente é barato, com uma assinatura nova não é.

Armadilhas que invalidam a leitura dos dados

Sintoma Causa provável O que fazer
Desafiante sempre mais lento Latência da máquina de teste Rode no servidor, não no notebook
Taxas muito distantes Amostra pequena Acumule mais de 50 resoluções por lado
Token aceito de um lado só Token expirou no caminho Consuma o token logo após recebê-lo
Erros só no pico Threads do plano esgotadas Compare o pico com as threads contratadas

Logs, LGPD e escopo autorizado

Grave apenas captcha_id, tempo, código de erro e o rótulo do provedor — nunca o conteúdo do formulário. Esses registros entram no inventário de dados da LGPD (RGPD em Portugal), então defina a retenção no primeiro dia. E mantenha o teste no escopo autorizado: sitekeys que você opera ou endpoints de QA em staging.example.com.

Perguntas frequentes

Dá para rodar a execução paralela sem duplicar o custo?

Na comparação, não: o mesmo desafio é resolvido dos dois lados e os dois cobram. O controle é a janela — de três a cinco dias bastam. Da rampa em diante o gasto duplo acaba.

Como escolho o plano com os números do teste?

Pelo pico de simultaneidade, não pelo volume mensal. Conte quantas resoluções ficam em voo ao mesmo tempo no horário mais carregado e contrate com folga acima disso.

O backend recusou um token que chegou rápido. Conta como falha?

Conta — para o usuário final o efeito é o mesmo. Só classifique o erro certo: confirme que o token vai no campo g-recaptcha-response e que não expirou até o envio do formulário.

Preciso desligar o provedor antigo no dia do corte?

Não. Deixe a integração antiga atrás de uma variável de ambiente por mais um ciclo e só remova o código após uma semana estável.

Leituras relacionadas

Próximos passos

Suba o roteador, rode 50 resoluções em paralelo e leve a tabela de métricas para a reunião de decisão. Crie sua conta na CaptchaAI e compare com o seu tráfego.

Guias de migração:

Os comentários estão desativados para este artigo.