Resolver um CAPTCHA de cada vez não escala. O reCAPTCHA v2 pode levar até 60 segundos para ser resolvido, e mil páginas em série somam quase 17 horas só de espera pela API. A saída é separar o envio das tarefas da consulta dos resultados — uma fila. Com o padrão certo, dezenas ou centenas de CAPTCHAs são resolvidos em paralelo, limitados apenas pelo número de threads do seu plano CaptchaAI. Este guia mostra quatro implementações prontas para produção, da mais simples (threading) até a fila com prioridade.
Por que resolver CAPTCHA em fila muda o resultado
Enviar e esperar, um de cada vez, é o padrão mais lento possível: a maior parte do tempo o processo fica parado aguardando a resposta de res.php, sem fazer nada. Uma fila resolve isso separando responsabilidades:
- Envia todos os CAPTCHAs para
in.phpsem esperar a resposta do anterior - Consulta (polling) vários IDs de tarefa em paralelo até cada um retornar
status: 1 - Faz nova tentativa automaticamente quando uma tarefa falha, sem intervenção manual
- Limita a simultaneidade para não ultrapassar as threads do seu plano CaptchaAI
- Acompanha o progresso e dispara callbacks quando cada tarefa termina
Se os workers rodam fora do Brasil, o RTT até a API soma alguns segundos por chamada; hospedar os workers em uma região próxima, como sa-east-1 (São Paulo) na AWS, reduz essa latência quando o volume é alto.
Fila com threading: paralelismo simples para código síncrono
- Use este padrão se o resto do projeto já é síncrono — cada worker roda em uma
Threadprópria - Puxa tarefas de uma
Queuepadrão do Python e chama a API de forma independente, sem bloquear os outros workers nores.php - Simples de depurar e suficiente até algumas dezenas de workers simultâneos
import time
import threading
import requests
from queue import Queue, Empty
API_KEY = "YOUR_API_KEY"
class CaptchaQueue:
"""Thread-based CAPTCHA solving queue."""
def __init__(self, api_key, max_workers=10):
self.api_key = api_key
self.task_queue = Queue()
self.result_queue = Queue()
self.max_workers = max_workers
self.workers = []
def submit(self, method, callback=None, **params):
"""Add a CAPTCHA task to the queue."""
task = {
"method": method,
"params": params,
"callback": callback,
}
self.task_queue.put(task)
def start(self):
"""Start worker threads."""
for _ in range(self.max_workers):
t = threading.Thread(target=self._worker, daemon=True)
t.start()
self.workers.append(t)
def wait(self):
"""Wait for all tasks to complete."""
self.task_queue.join()
def get_results(self):
"""Get all available results."""
results = []
while not self.result_queue.empty():
try:
results.append(self.result_queue.get_nowait())
except Empty:
break
return results
def _worker(self):
while True:
try:
task = self.task_queue.get(timeout=1)
except Empty:
continue
try:
result = self._solve(task["method"], **task["params"])
entry = {"status": "solved", "result": result, "task": task}
self.result_queue.put(entry)
if task["callback"]:
task["callback"](result)
except Exception as e:
entry = {"status": "error", "error": str(e), "task": task}
self.result_queue.put(entry)
finally:
self.task_queue.task_done()
def _solve(self, method, **params):
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key, "method": method, "json": 1, **params,
}, timeout=30).json()
if submit.get("status") != 1:
raise Exception(f"Submit error: {submit.get('request')}")
task_id = submit["request"]
for _ in range(30):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id, "json": 1,
}, timeout=30).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise Exception("CAPTCHA unsolvable")
raise TimeoutError("Solve timed out")
# Usage
queue = CaptchaQueue(API_KEY, max_workers=5)
queue.start()
# Submit multiple CAPTCHAs
urls_and_sitekeys = [
("https://example.com/page1", "SITEKEY_1"),
("https://example.com/page2", "SITEKEY_2"),
("https://example.com/page3", "SITEKEY_3"),
]
for url, sitekey in urls_and_sitekeys:
queue.submit("userrecaptcha", googlekey=sitekey, pageurl=url)
queue.wait()
results = queue.get_results()
print(f"Solved {len(results)} CAPTCHAs")
for r in results:
print(f" {r['status']}: {r.get('result', r.get('error', ''))[:50]}")
Fila assíncrona com asyncio: mais throughput por menos overhead
Use
asyncioquando threads do sistema operacional começam a pesar: uma única thread comSemaphorecontrola quantas tarefas estão em voo — menos overhead por tarefa, mais fácil escalar para centenas de CAPTCHAs. Escolha natural em stacks já assíncronas (aiohttp, FastAPI).
import asyncio
import aiohttp
API_KEY = "YOUR_API_KEY"
class AsyncCaptchaQueue:
"""Async CAPTCHA solving queue with concurrency control."""
def __init__(self, api_key, max_concurrent=10):
self.api_key = api_key
self.semaphore = asyncio.Semaphore(max_concurrent)
self.results = []
async def solve_batch(self, tasks):
"""Solve a batch of CAPTCHA tasks concurrently."""
coros = [self._solve_task(task) for task in tasks]
self.results = await asyncio.gather(*coros, return_exceptions=True)
return self.results
async def _solve_task(self, task):
async with self.semaphore:
return await self._solve(task["method"], **task["params"])
async def _solve(self, method, **params):
async with aiohttp.ClientSession() as session:
# Submit
async with session.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key, "method": method, "json": 1, **params,
}) as resp:
data = await resp.json(content_type=None)
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
# Poll
for _ in range(30):
await asyncio.sleep(5)
async with session.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id, "json": 1,
}) as resp:
result = await resp.json(content_type=None)
if result.get("status") == 1:
return result["request"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise Exception("CAPTCHA unsolvable")
raise TimeoutError("Solve timed out")
# Usage
async def main():
queue = AsyncCaptchaQueue(API_KEY, max_concurrent=5)
tasks = [
{"method": "userrecaptcha", "params": {"googlekey": f"SITEKEY_{i}", "pageurl": f"https://example.com/page{i}"}}
for i in range(10)
]
results = await queue.solve_batch(tasks)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"Task {i}: ERROR — {result}")
else:
print(f"Task {i}: {result[:50]}...")
asyncio.run(main())
Padrão produtor-consumidor para scraping contínuo
- O
producedescobre e enfileira as tarefas conforme o crawl avança, sem esperar que ele termine - Um número fixo de
consumelê de umaasyncio.Queuelimitada e resolve os CAPTCHAs - O tamanho máximo da fila evita que a memória cresça sem controle durante o crawl
import asyncio
import aiohttp
API_KEY = "YOUR_API_KEY"
class ProducerConsumerQueue:
"""Continuous CAPTCHA solving with producer-consumer pattern."""
def __init__(self, api_key, queue_size=100, num_consumers=5):
self.api_key = api_key
self.queue = asyncio.Queue(maxsize=queue_size)
self.num_consumers = num_consumers
self.solved_count = 0
self.error_count = 0
self.running = True
async def produce(self, tasks):
"""Producer: feed CAPTCHA tasks into the queue."""
for task in tasks:
await self.queue.put(task)
# Signal consumers to stop
for _ in range(self.num_consumers):
await self.queue.put(None)
async def consume(self, result_handler):
"""Consumer: solve CAPTCHAs and call result handler."""
async with aiohttp.ClientSession() as session:
while True:
task = await self.queue.get()
if task is None:
self.queue.task_done()
break
try:
result = await self._solve(session, task["method"], **task["params"])
self.solved_count += 1
if result_handler:
await result_handler(task, result)
except Exception as e:
self.error_count += 1
print(f"Error: {e}")
finally:
self.queue.task_done()
async def run(self, tasks, result_handler=None):
"""Run the producer-consumer pipeline."""
# Start producer
producer = asyncio.create_task(self.produce(tasks))
# Start consumers
consumers = [
asyncio.create_task(self.consume(result_handler))
for _ in range(self.num_consumers)
]
# Wait for everything to finish
await producer
await asyncio.gather(*consumers)
print(f"Complete: {self.solved_count} solved, {self.error_count} errors")
async def _solve(self, session, method, **params):
async with session.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key, "method": method, "json": 1, **params,
}) as resp:
data = await resp.json(content_type=None)
if data.get("status") != 1:
raise Exception(f"Submit: {data.get('request')}")
task_id = data["request"]
for _ in range(30):
await asyncio.sleep(5)
async with session.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id, "json": 1,
}) as resp:
result = await resp.json(content_type=None)
if result.get("status") == 1:
return result["request"]
raise TimeoutError("Timed out")
# Usage
async def handle_result(task, token):
url = task["params"]["pageurl"]
print(f"Solved for {url}: {token[:30]}...")
async def main():
queue = ProducerConsumerQueue(API_KEY, num_consumers=5)
tasks = [
{"method": "userrecaptcha", "params": {"googlekey": f"SITEKEY_{i}", "pageurl": f"https://example.com/page{i}"}}
for i in range(20)
]
await queue.run(tasks, result_handler=handle_result)
asyncio.run(main())
Fila com prioridade: o checkout não pode esperar
Nem toda tarefa tem a mesma urgência: um Cloudflare Turnstile no checkout de um fluxo de QA autorizado não pode esperar atrás de cem páginas de catálogo. A
PriorityQueueresolve isso — cada tarefa carrega um número de prioridade (menor valor primeiro) e os workers puxam sempre a mais urgente primeiro.
import asyncio
from dataclasses import dataclass, field
API_KEY = "YOUR_API_KEY"
@dataclass(order=True)
class PriorityTask:
priority: int
task: dict = field(compare=False)
class PriorityCaptchaQueue:
"""CAPTCHA queue with priority levels."""
def __init__(self, api_key, num_workers=5):
self.api_key = api_key
self.queue = asyncio.PriorityQueue()
self.num_workers = num_workers
self.results = {}
async def submit(self, task_id, method, priority=5, **params):
"""Submit with priority (lower number = higher priority)."""
await self.queue.put(PriorityTask(
priority=priority,
task={"id": task_id, "method": method, "params": params},
))
async def process(self):
"""Process all queued tasks by priority."""
workers = [asyncio.create_task(self._worker()) for _ in range(self.num_workers)]
# Wait for queue to drain
await self.queue.join()
# Cancel workers
for w in workers:
w.cancel()
return self.results
async def _worker(self):
import aiohttp
async with aiohttp.ClientSession() as session:
while True:
item = await self.queue.get()
task = item.task
try:
result = await self._solve(session, task["method"], **task["params"])
self.results[task["id"]] = {"status": "solved", "token": result}
except Exception as e:
self.results[task["id"]] = {"status": "error", "error": str(e)}
finally:
self.queue.task_done()
async def _solve(self, session, method, **params):
import aiohttp
async with session.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key, "method": method, "json": 1, **params,
}) as resp:
data = await resp.json(content_type=None)
if data.get("status") != 1:
raise Exception(data.get("request"))
task_id = data["request"]
for _ in range(30):
await asyncio.sleep(5)
async with session.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id, "json": 1,
}) as resp:
result = await resp.json(content_type=None)
if result.get("status") == 1:
return result["request"]
raise TimeoutError()
# Usage
async def main():
pq = PriorityCaptchaQueue(API_KEY, num_workers=3)
# High priority — checkout pages
await pq.submit("checkout_1", "turnstile", priority=1, sitekey="KEY", pageurl="https://shop.com/checkout")
# Normal priority — product pages
for i in range(5):
await pq.submit(f"product_{i}", "userrecaptcha", priority=5, googlekey="KEY", pageurl=f"https://shop.com/p/{i}")
# Low priority — info pages
for i in range(3):
await pq.submit(f"info_{i}", "userrecaptcha", priority=10, googlekey="KEY", pageurl=f"https://shop.com/info/{i}")
results = await pq.process()
for task_id, result in results.items():
print(f"{task_id}: {result['status']}")
asyncio.run(main())
Monitoramento e métricas: enxergando a fila em produção
Meça pelo menos quatro números antes de ir para produção: tarefas enviadas, resolvidas, com falha, e o throughput por minuto. Um objeto de métricas simples como o abaixo já basta para perceber uma queda na taxa de sucesso antes que ela vire um incidente maior.
import time
from dataclasses import dataclass, field
@dataclass
class QueueMetrics:
submitted: int = 0
solved: int = 0
failed: int = 0
total_solve_time: float = 0.0
start_time: float = field(default_factory=time.time)
@property
def avg_solve_time(self):
return self.total_solve_time / self.solved if self.solved else 0
@property
def success_rate(self):
total = self.solved + self.failed
return (self.solved / total * 100) if total else 0
@property
def throughput(self):
elapsed = time.time() - self.start_time
return self.solved / elapsed * 60 if elapsed > 0 else 0
def report(self):
return (
f"Submitted: {self.submitted} | "
f"Solved: {self.solved} | "
f"Failed: {self.failed} | "
f"Avg time: {self.avg_solve_time:.1f}s | "
f"Success: {self.success_rate:.1f}% | "
f"Throughput: {self.throughput:.0f}/min"
)
Solução de problemas comuns na fila
Os sintomas abaixo aparecem com frequência em filas de CAPTCHA em produção. Na maioria dos casos, a causa está na simultaneidade configurada ou em uma exceção não tratada no worker.
| Sintoma | Causa | Correção |
|---|---|---|
| A fila cresce, mas as tarefas não terminam | Workers demais sobrecarregam a API | Reduza max_workers/max_concurrent |
ERROR_NO_SLOT_AVAILABLE |
Limite de simultaneidade do plano foi atingido | Adicione um intervalo entre os envios |
| Tarefas presas na fila | Threads do worker morreram por uma exceção não tratada | Envolva o loop do worker em try/except |
| A memória cresce com o tempo | Resultados não foram consumidos | Chame get_results() periodicamente |
| Fila assíncrona trava | Falta um await em alguma chamada |
Confira se toda chamada assíncrona está com await |
Perguntas frequentes
Quantas threads minha fila pode usar ao mesmo tempo?
A CaptchaAI limita a simultaneidade pelo plano, não pela sua fila: BASIC (US$ 15/mês, 5 threads) e STANDARD (US$ 30/mês, 15 threads) já bastam para testes; para volume maior, suba para ADVANCE (US$ 90/mês, 50 threads) ou além, até VIP-3 (US$ 7500/mês, 5000 threads). Configure max_workers (threading) ou max_concurrent (asyncio) próximo do número de threads do plano — passar disso só devolve mais ERROR_NO_SLOT_AVAILABLE.
Threading ou asyncio: qual escolher em um projeto novo?
Use asyncio: ele lida com a espera por I/O — a chamada à API, o polling — de forma muito mais leve que threads do sistema operacional, e já é o padrão em stacks como FastAPI e aiohttp. Threading continua sendo a opção certa quando você está integrando a fila a uma base de código síncrona existente e reescrever tudo em asyncio não compensa.
O worker travou sem lançar erro — o que verificar primeiro?
Confirme que o loop do worker está dentro de um try/except: uma exceção não tratada mata a thread silenciosamente e a tarefa fica presa na fila para sempre. Depois, olhe os timeouts do requests ou do aiohttp — uma chamada sem timeout configurado tem o mesmo efeito de travar o worker indefinidamente.
Como evito perder tarefas se o processo cair no meio da fila?
Registre o task_id retornado por in.php assim que ele chega, antes de começar o polling. Se o processo cair, você consulta res.php com os IDs já enviados em vez de reenviar tarefas que talvez já tenham sido resolvidas do lado da CaptchaAI.
Preciso de bibliotecas além de requests e aiohttp?
Não, para reproduzir os exemplos deste guia: requests cobre a fila com threading e aiohttp cobre as versões assíncronas — ambas fazem parte do ecossistema padrão de Python para chamadas HTTP e não exigem nenhuma dependência adicional.
Resumo
O padrão que você escolhe importa menos que o princípio por trás dele: nunca espere uma resposta de CAPTCHA por vez. Separe o envio da consulta, dimensione os workers pelas threads do seu plano na CaptchaAI e meça antes de aumentar a simultaneidade. Comece pelo threading se o resto do código já é síncrono, migre para asyncio à medida que o volume cresce, e reserve o produtor-consumidor para crawls contínuos.