Tutorials

Health check em workers de CAPTCHA: liveness, readiness e dependências

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:

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