Quando o volume de CAPTCHAs oscila — milhares de requisições no pico de uma campanha, quase nada de madrugada — manter uma frota fixa de servidores ligada o tempo todo é desperdício. A resposta é uma fila: um produtor empilha tarefas no Redis, um conjunto de workers rodando no Kubernetes consome essa fila e cada desafio é resolvido pela API da CaptchaAI. Quando a fila cresce, o cluster sobe mais pods; quando ela esvazia, ele recolhe. Este guia monta esse padrão do zero, com deployment, autoescalonamento e o código do worker prontos para colar.
Arquitetura da fila: produtor, Redis e workers
O desenho separa quem gera trabalho de quem executa. O produtor não fala com a CaptchaAI diretamente: ele só coloca tarefas no Redis. Os workers puxam da fila no próprio ritmo, resolvem o CAPTCHA e gravam o token em um segundo armazenamento no Redis. Esse desacoplamento é o que permite escalar horizontalmente — a fila absorve os picos sem derrubar ninguém.
Producer → Redis Queue → Worker Pods (auto-scaled) → CaptchaAI API
↓
Results Store (Redis)
Deployment dos workers de CAPTCHA
O Deployment define quantos pods de worker sobem e com qual perfil de recursos. Comece com três réplicas: é o suficiente para um fluxo constante e dá margem para o autoescalonamento crescer a partir daí. Cada pod recebe a chave de API por referência a um Secret (nunca embutida na imagem) e a URL do Redis por variável de ambiente. Os limites de memória e CPU são enxutos de propósito — um worker de CAPTCHA passa quase todo o tempo aguardando a resposta da API, então o gargalo é a rede, não o processamento local.
# k8s/worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: captcha-worker
labels:
app: captcha-worker
spec:
replicas: 3
selector:
matchLabels:
app: captcha-worker
template:
metadata:
labels:
app: captcha-worker
spec:
containers:
- name: worker
image: your-registry/captcha-worker:latest
env:
- name: CAPTCHAAI_KEY
valueFrom:
secretKeyRef:
name: captchaai-secret
key: api-key
- name: REDIS_URL
value: "redis://redis-service:6379"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
Segredo com a chave de API
A chave de API nunca deve viver na imagem do contêiner nem no YAML versionado. Guarde-a em um Secret do Kubernetes e deixe o Deployment injetá-la em tempo de execução. Um comando cria o segredo a partir da chave — substitua YOUR_API_KEY pela sua chave real do painel da CaptchaAI:
kubectl create secret generic captchaai-secret \
--from-literal=api-key=YOUR_API_KEY
Redis como fila de tarefas
O Redis faz dois papéis aqui: é a fila de entrada (captcha:queue) e o armazenamento de resultados (captcha:results). Para desenvolvimento e testes, uma única réplica com a imagem redis:7-alpine basta. O Service redis-service dá um nome DNS estável para que os workers encontrem o Redis sem depender do IP do pod. Em produção com carga real, troque esse pod solto por uma instância com persistência e alta disponibilidade (um Redis gerenciado ou um operador no cluster) — a fila é o ponto único de falha do desenho.
# k8s/redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
requests:
memory: "128Mi"
cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
name: redis-service
spec:
selector:
app: redis
ports:
- port: 6379
Código do worker: consumir a fila e resolver
O worker é um laço simples. Ele bloqueia em blpop esperando a próxima tarefa, envia o CAPTCHA para o endpoint in.php, consulta o resultado em res.php a cada cinco segundos até o token ficar pronto e grava o desfecho (sucesso ou erro) no hash captcha:results. No fim de cada volta, ele atualiza a métrica captcha:queue_length — é esse número que o autoescalonador observa. Como toda a comunicação usa json=1 e os campos padrão da API, o mesmo worker resolve qualquer tipo suportado sem mudar o código.
# worker.py
import os
import json
import time
import redis
import requests
class CaptchaWorker:
"""Kubernetes worker that processes CAPTCHA tasks from Redis."""
def __init__(self):
self.api_key = os.environ["CAPTCHAAI_KEY"]
self.redis = redis.from_url(
os.environ.get("REDIS_URL", "redis://localhost:6379"),
)
self.base = "https://ocr.captchaai.com"
def run(self):
"""Main worker loop."""
hostname = os.environ.get("HOSTNAME", "unknown")
print(f"Worker {hostname} started")
while True:
result = self.redis.blpop("captcha:queue", timeout=30)
if result is None:
continue
_, raw = result
task = json.loads(raw)
task_id = task.get("id", "unknown")
print(f"[{hostname}] Processing {task_id}")
start = time.time()
try:
token = self._solve(task["method"], task["params"])
duration = time.time() - start
self.redis.hset("captcha:results", task_id, json.dumps({
"status": "success",
"token": token,
"duration": f"{duration:.1f}s",
"worker": hostname,
}))
print(f"[{hostname}] {task_id} solved in {duration:.1f}s")
except Exception as e:
self.redis.hset("captcha:results", task_id, json.dumps({
"status": "error",
"error": str(e),
"worker": hostname,
}))
print(f"[{hostname}] {task_id} failed: {e}")
# Update queue length metric
queue_len = self.redis.llen("captcha:queue")
self.redis.set("captcha:queue_length", queue_len)
def _solve(self, method, params, timeout=120):
resp = requests.post(f"{self.base}/in.php", data={
"key": self.api_key,
"method": method,
"json": 1,
**params,
}, timeout=30)
result = resp.json()
if result.get("status") != 1:
raise RuntimeError(result.get("request"))
captcha_id = result["request"]
start = time.time()
while time.time() - start < timeout:
time.sleep(5)
resp = requests.get(f"{self.base}/res.php", params={
"key": self.api_key,
"action": "get",
"id": captcha_id,
"json": 1,
}, timeout=15)
data = resp.json()
if data["request"] != "CAPCHA_NOT_READY":
if data.get("status") == 1:
return data["request"]
raise RuntimeError(data["request"])
raise TimeoutError("Solve timeout")
if __name__ == "__main__":
CaptchaWorker().run()
Autoescalonamento por profundidade de fila (HPA)
O número de réplicas não deve ser fixo: ele precisa acompanhar o tamanho da fila. O HorizontalPodAutoscaler usa uma métrica externa — redis_queue_length — e mantém uma média de dez tarefas em fila por pod (averageValue: "10"). Se a fila dispara para 200 itens, o HPA caminha em direção às 20 réplicas do teto; quando ela drena, ele volta para as duas réplicas mínimas. Para o HPA enxergar essa métrica, você precisa de um adaptador de métricas externas ou, mais simples, do KEDA, que lê o comprimento da fila Redis nativamente.
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: captcha-worker-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: captcha-worker
minReplicas: 2
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: redis_queue_length
selector:
matchLabels:
queue: captcha
target:
type: AverageValue
averageValue: "10"
Vale lembrar que os pods não são o único limite de vazão. A CaptchaAI cobra por threads simultâneas — cada thread é um CAPTCHA em processamento — e não por resolução. O ADVANCE (US$ 90/mês, 50 threads) sustenta até 50 resoluções em paralelo, independentemente de quantos workers você suba. Subir 80 pods contra um teto de 50 threads só gera espera; dimensione o maxReplicas conforme o plano contratado, do BASIC (US$ 15/mês, 5 threads) aos tiers VIP.
Produtor: enfileirar e coletar resultados
Do outro lado da fila fica o produtor. submit_tasks gera um identificador curto para cada tarefa, faz rpush na fila e devolve os IDs. get_results consulta o hash de resultados a cada segundo até coletar tudo ou estourar o timeout. Esse par de funções é o que sua aplicação chama: ela fala apenas com o Redis, e os workers fazem o resto.
import json
import uuid
import redis
def submit_tasks(redis_url, tasks):
"""Submit CAPTCHA tasks to the queue."""
r = redis.from_url(redis_url)
task_ids = []
for task in tasks:
task_id = str(uuid.uuid4())[:8]
task["id"] = task_id
r.rpush("captcha:queue", json.dumps(task))
task_ids.append(task_id)
return task_ids
def get_results(redis_url, task_ids, timeout=180):
"""Wait for and collect results."""
r = redis.from_url(redis_url)
results = {}
deadline = time.time() + timeout
while len(results) < len(task_ids) and time.time() < deadline:
for tid in task_ids:
if tid in results:
continue
raw = r.hget("captcha:results", tid)
if raw:
results[tid] = json.loads(raw)
time.sleep(1)
return results
Considerações de produção
Se o seu tráfego é majoritariamente brasileiro, rode o cluster e a fila próximos dos usuários — uma região como a sa-east-1 (São Paulo) reduz o RTT entre produtor, workers e Redis. Antes de subir, valide o comportamento do HPA sob carga em um ambiente de staging autorizado, com dados fictícios e endpoints internos. E quando o pipeline processar dados que acompanham os CAPTCHAs, considere as obrigações da LGPD (ou do RGPD, em Portugal) ao definir o que os workers registram em log.
Solução de problemas
Os sintomas mais comuns aparecem no primeiro deploy. Use a tabela como triagem rápida:
| Problema | Causa | Correção |
|---|---|---|
| Workers não sobem | Segredo não foi criado | Rode o comando kubectl create secret |
| Pods em CrashLoopBackOff | Variáveis de ambiente ou Redis ausentes | Verifique os logs com kubectl logs |
| HPA não escala | Métricas externas não configuradas | Instale um adaptador de métricas (KEDA) |
| Fila cresce, mas nada é processado | Workers ociosos ou travados | Cheque a saúde dos pods e reinicie |
Perguntas frequentes
Como dimensiono o plano da CaptchaAI para o número de workers?
Alinhe o maxReplicas ao teto de threads do seu plano. Como a cobrança é por thread simultânea (não por resolução), subir mais pods do que o número de threads disponíveis só cria fila de espera. O ADVANCE (US$ 90/mês, 50 threads) comporta até 50 resoluções em paralelo.
O HPA consegue reduzir a zero quando a fila esvazia?
O HorizontalPodAutoscaler puro não escala até zero — o mínimo aqui é duas réplicas. Se você quer chegar a zero pod em períodos ociosos, use o KEDA, que suporta scale-to-zero a partir do comprimento da fila Redis.
Onde devo rodar os workers para reduzir a latência?
Escolha uma região próxima da origem do seu tráfego. Para usuários no Brasil, sa-east-1 (São Paulo) costuma dar o menor RTT entre a aplicação, o Redis e a API. Mantenha workers, fila e produtor na mesma região para evitar saltos desnecessários.
Preciso me preocupar com a LGPD ao processar tarefas em fila?
Se as tarefas carregam dados pessoais, sim: minimize o que é gravado em log e defina retenção curta para o hash de resultados. Trate a conformidade como decisão de arquitetura, não como algo a resolver depois — e nunca a interprete como aconselhamento jurídico.
Guias relacionados
Escale para milhares de resoluções — crie sua conta na CaptchaAI e rode em Kubernetes.