Um worker de CAPTCHA "de pé" não é o mesmo que um worker saudável: o processo pode estar rodando, consumindo CPU e memória, e mesmo assim não resolver um único desafio há dez minutos — chave de API zerada, dependência fora do ar ou um loop travado em silêncio. Sem endpoints de health check, o orquestrador continua mandando trabalho para esse worker morto. Com /health/live, /health/ready e /health/dependencies, o Kubernetes e o load balancer sabem exatamente quando parar de rotear tarefas e quando reiniciar o contêiner — a diferença entre um incidente detectado em segundos e um que só aparece no relatório de SLA do dia seguinte.
Isso importa ainda mais quando os workers rodam fora dos Estados Unidos — por exemplo, em sa-east-1 (São Paulo), onde a latência até a API da CaptchaAI já é alguns milissegundos maior por padrão. Um dependency check mal calibrado nesse cenário pode confundir uma variação normal de rede com uma dependência fora do ar, então vale ajustar os thresholds abaixo ao ambiente real antes de ir para produção.
Liveness, readiness ou dependency check: qual usar
| Verificação | Pergunta que ela responde | Ação quando falha |
|---|---|---|
| Liveness | O processo ainda está rodando? | Reiniciar o contêiner |
| Readiness | O worker pode aceitar trabalho agora? | Parar de rotear tráfego |
| Dependency | Os serviços upstream estão OK? | Degradar com segurança |
O que os códigos 200 e 503 significam em cada endpoint
| Endpoint | 200 | 503 |
|---|---|---|
/health/live |
Processo responsivo | Processo travado — reinicie |
/health/ready |
Pode aceitar trabalho | Pare de enviar tarefas |
/health/dependencies |
Todas as dependências OK | Upstream degradado |
Implementação em Python: endpoints de health check com Flask
Este exemplo expõe os três endpoints com Flask, armazena o saldo da conta em cache por 60 segundos e acompanha falhas consecutivas para decidir a prontidão do worker.
import requests
import time
import threading
from flask import Flask, jsonify
from dataclasses import dataclass, field
API_KEY = "YOUR_API_KEY"
RESULT_URL = "https://ocr.captchaai.com/res.php"
app = Flask(__name__)
@dataclass
class WorkerHealth:
"""Tracks worker health metrics."""
started_at: float = field(default_factory=time.monotonic)
last_solve_at: float = 0.0
total_solved: int = 0
total_failed: int = 0
consecutive_failures: int = 0
balance: float | None = None
balance_checked_at: float = 0.0
_lock: threading.Lock = field(default_factory=threading.Lock)
def record_success(self):
with self._lock:
self.total_solved += 1
self.last_solve_at = time.monotonic()
self.consecutive_failures = 0
def record_failure(self):
with self._lock:
self.total_failed += 1
self.consecutive_failures += 1
@property
def success_rate(self) -> float:
total = self.total_solved + self.total_failed
return self.total_solved / total if total > 0 else 1.0
@property
def seconds_since_last_solve(self) -> float:
if self.last_solve_at == 0:
return time.monotonic() - self.started_at
return time.monotonic() - self.last_solve_at
health = WorkerHealth()
# Thresholds
MAX_CONSECUTIVE_FAILURES = 10
MAX_SECONDS_WITHOUT_SOLVE = 600 # 10 minutes
MIN_BALANCE = 1.0
def check_balance() -> float | None:
"""Check CaptchaAI balance."""
now = time.monotonic()
# Cache balance for 60 seconds
if health.balance is not None and now - health.balance_checked_at < 60:
return health.balance
try:
resp = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "getbalance", "json": 1,
}, timeout=10).json()
health.balance = float(resp.get("request", 0))
health.balance_checked_at = now
return health.balance
except Exception:
return health.balance # Return cached value on error
@app.route("/health/live")
def liveness():
"""Liveness probe — is the process responsive?"""
return jsonify({"status": "ok", "uptime_s": int(time.monotonic() - health.started_at)}), 200
@app.route("/health/ready")
def readiness():
"""Readiness probe — can the worker accept tasks?"""
issues = []
# Check consecutive failures
if health.consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
issues.append(f"consecutive_failures={health.consecutive_failures}")
# Check time since last solve
if health.total_solved > 0 and health.seconds_since_last_solve > MAX_SECONDS_WITHOUT_SOLVE:
issues.append(f"no_solve_for={int(health.seconds_since_last_solve)}s")
# Check balance
balance = check_balance()
if balance is not None and balance < MIN_BALANCE:
issues.append(f"low_balance=${balance:.2f}")
if issues:
return jsonify({
"status": "not_ready",
"issues": issues,
"stats": {
"solved": health.total_solved,
"failed": health.total_failed,
"success_rate": round(health.success_rate, 3),
},
}), 503
return jsonify({
"status": "ready",
"stats": {
"solved": health.total_solved,
"failed": health.total_failed,
"success_rate": round(health.success_rate, 3),
"balance": balance,
},
}), 200
@app.route("/health/dependencies")
def dependencies():
"""Check upstream dependencies."""
checks = {}
# CaptchaAI API reachability
try:
resp = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "getbalance", "json": 1,
}, timeout=10)
checks["captchaai_api"] = {
"status": "ok" if resp.status_code == 200 else "degraded",
"response_ms": int(resp.elapsed.total_seconds() * 1000),
}
except Exception as e:
checks["captchaai_api"] = {"status": "down", "error": str(e)}
all_ok = all(c["status"] == "ok" for c in checks.values())
return jsonify({
"status": "ok" if all_ok else "degraded",
"checks": checks,
}), 200 if all_ok else 503
# --- Worker loop (runs in background) ---
def worker_loop():
"""Simulated CAPTCHA solving worker."""
while True:
try:
# ... solve CAPTCHA logic ...
health.record_success()
except Exception:
health.record_failure()
time.sleep(1)
threading.Thread(target=worker_loop, daemon=True).start()
Implementação em Node.js: endpoints de health check com Express
A mesma lógica em Node.js com Express — mesmos limites, mesmo cache de saldo, agora com async/await no lugar de threads.
const express = require("express");
const API_KEY = "YOUR_API_KEY";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
const app = express();
const health = {
startedAt: Date.now(),
lastSolveAt: 0,
totalSolved: 0,
totalFailed: 0,
consecutiveFailures: 0,
balance: null,
balanceCheckedAt: 0,
recordSuccess() {
this.totalSolved++;
this.lastSolveAt = Date.now();
this.consecutiveFailures = 0;
},
recordFailure() {
this.totalFailed++;
this.consecutiveFailures++;
},
get successRate() {
const total = this.totalSolved + this.totalFailed;
return total > 0 ? this.totalSolved / total : 1;
},
};
async function checkBalance() {
if (health.balance !== null && Date.now() - health.balanceCheckedAt < 60000) {
return health.balance;
}
try {
const url = `${RESULT_URL}?key=${API_KEY}&action=getbalance&json=1`;
const resp = await (await fetch(url)).json();
health.balance = parseFloat(resp.request);
health.balanceCheckedAt = Date.now();
return health.balance;
} catch {
return health.balance;
}
}
app.get("/health/live", (req, res) => {
res.json({ status: "ok", uptimeMs: Date.now() - health.startedAt });
});
app.get("/health/ready", async (req, res) => {
const issues = [];
if (health.consecutiveFailures >= 10) {
issues.push(`consecutive_failures=${health.consecutiveFailures}`);
}
if (health.totalSolved > 0) {
const silentMs = Date.now() - health.lastSolveAt;
if (silentMs > 600_000) {
issues.push(`no_solve_for=${Math.round(silentMs / 1000)}s`);
}
}
const balance = await checkBalance();
if (balance !== null && balance < 1.0) {
issues.push(`low_balance=$${balance.toFixed(2)}`);
}
const stats = {
solved: health.totalSolved,
failed: health.totalFailed,
successRate: Math.round(health.successRate * 1000) / 1000,
balance,
};
if (issues.length > 0) {
return res.status(503).json({ status: "not_ready", issues, stats });
}
res.json({ status: "ready", stats });
});
app.get("/health/dependencies", async (req, res) => {
const checks = {};
try {
const start = Date.now();
const url = `${RESULT_URL}?key=${API_KEY}&action=getbalance&json=1`;
const resp = await fetch(url);
checks.captchaaiApi = {
status: resp.ok ? "ok" : "degraded",
responseMs: Date.now() - start,
};
} catch (e) {
checks.captchaaiApi = { status: "down", error: e.message };
}
const allOk = Object.values(checks).every((c) => c.status === "ok");
res.status(allOk ? 200 : 503).json({
status: allOk ? "ok" : "degraded",
checks,
});
});
app.listen(8080, () => console.log("Health server on :8080"));
Probes de liveness e readiness no Kubernetes
Com os endpoints no ar, a configuração do Deployment aponta o Kubernetes para as probes certas: livenessProbe em /health/live e readinessProbe em /health/ready, cada uma com seu próprio intervalo e threshold de falha.
apiVersion: apps/v1
kind: Deployment
metadata:
name: captcha-worker
spec:
replicas: 3
template:
spec:
containers:
- name: worker
image: captcha-worker:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 2
Limites do operador
- Use a readiness para bloquear novo trabalho, a liveness para acionar o reinício, e alertas de saldo para capturar queda de produtividade antes que ela vire incidente.
- Amarre os health checks à profundidade da fila, à taxa de erro recente e à acessibilidade das dependências — não apenas ao uptime bruto do processo.
- Deixe os thresholds visíveis para quem está de plantão, para que uma mudança de status seja acionável e não apenas ruído no painel.
Problemas comuns e como corrigir
| Problema | Causa | Correção |
|---|---|---|
| Worker reiniciando o tempo todo | Threshold de liveness baixo demais | Aumente failureThreshold ou periodSeconds |
| Worker marcado como not-ready na inicialização | Nenhuma resolução ainda conta como "demorou demais" | Só cheque seconds_since_last_solve depois da primeira resolução |
| Checagem de saldo deixa o health check lento | Chamada à API em toda requisição | Cacheie o saldo com um TTL (60 s é um bom padrão) |
| O próprio endpoint de health check quebra | Exceção não tratada dentro do check | Envolva cada checagem em try/except; devolva degraded em vez de 500 |
| Falsos negativos no dependency check | Instabilidade de rede durante a checagem de saldo | Sirva o valor em cache com uma abordagem stale-while-revalidate |
Perguntas frequentes
Os endpoints de health check devem ficar acessíveis publicamente?
Não. Exponha /health/live, /health/ready e /health/dependencies apenas na rede interna do cluster ou atrás de uma allowlist — eles revelam saldo, taxa de sucesso e detalhes de dependências que não precisam estar públicos.
Qual o intervalo recomendado entre as probes de liveness e readiness?
Liveness a cada 10–30 s com failureThreshold: 3; readiness a cada 5–10 s com failureThreshold: 2. Probes mais frequentes detectam problemas mais rápido, mas aumentam a carga sobre o worker.
Como escolher o valor de MAX_SECONDS_WITHOUT_SOLVE?
Meça o tempo de resolução típico do seu volume de tarefas e aplique uma margem de segurança generosa — 600 s (10 minutos) funciona bem para workers com tráfego constante, mas um worker com picos esparsos pode precisar de um valor maior para evitar falsos positivos.
O que fazer quando /health/dependencies fica degradado, mas o worker continua vivo?
Trate isso como um sinal para um circuit breaker, não como motivo para reiniciar o processo: pare de bater na dependência instável, deixe o worker de pé e retome as chamadas só depois que ela se recuperar. Reiniciar o contêiner não resolve uma falha upstream.
Como agregar a saúde de vários workers em uma única visão?
Exponha métricas no formato Prometheus (/metrics) ao lado dos endpoints de health check e monte um painel no Grafana para ver a frota inteira — worker por worker ou agregada por status geral.
Artigos relacionados
Próximos passos
Deixe seus workers CAPTCHA prontos para produção — gere sua chave de API da CaptchaAI e adicione os três endpoints de health check antes do próximo deploy.
Guias relacionados: