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
- Como montar um pipeline de testes automatizados com a CaptchaAI
- CAPTCHA em CI/CD com GitHub Actions
- Como tratar CAPTCHA em testes E2E no Cypress
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: