Un worker de resolución de CAPTCHA puede tener el proceso en marcha y aun así estar inservible: la clave API se quedó sin saldo, la API upstream no responde o el bucle de resolución se atascó sin devolver ningún token. Un health check es justo lo que le permite a tu orquestador distinguir "el proceso vive" de "el proceso sirve". En esta guía expones tres endpoints —liveness, readiness y dependencias— para que Kubernetes o tu balanceador de carga saquen de rotación a un worker degradado antes de que siga recibiendo tareas que no puede resolver.
Piensa en una agencia de datos en Ciudad de México que corre una flota de 40 workers detrás de un balanceador, cada uno consumiendo tareas de una cola y llamando a la API de CaptchaAI. Sin health checks, cuando el saldo de una clave se agota a media tarde, el balanceador sigue enviando trabajo a ese worker durante minutos: las tareas fallan en silencio y la cola se acumula. Con un readiness que consulta el saldo, ese worker se marca not_ready en segundos y el tráfico se redirige solo a los que sí pueden resolver.
Liveness, readiness y dependencias: los tres checks
No todos los health checks responden la misma pregunta. Conviene separarlos porque cada uno dispara una acción distinta en el orquestador:
| Check | Qué pregunta | Acción ante el fallo |
|---|---|---|
| Liveness | ¿El proceso responde? | Reiniciar el contenedor |
| Readiness | ¿Puede aceptar trabajo ahora? | Dejar de enrutar tráfico |
| Dependencias | ¿La API upstream está bien? | Degradarse con gracia |
La regla práctica: liveness debe ser trivial y no llamar a nada externo, porque un reinicio es una acción cara. Readiness sí puede mirar el estado real (saldo, fallos consecutivos, tiempo sin resolver) y devolver 503 cuando el worker no está en condiciones, sin matar el proceso.
Python: endpoints de salud con Flask
El worker mantiene un objeto WorkerHealth protegido con un lock y actualiza sus contadores en cada intento de resolución. Los tres endpoints leen ese estado: /health/live responde al instante, /health/ready evalúa umbrales de fallos, tiempo sin resolver y saldo mínimo, y /health/dependencies mide la latencia hacia la API. El saldo se cachea 60 segundos para no golpear la API en cada sondeo.
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
# Practical operating rules
# - Mark the worker not ready if it cannot solve reliably right now.
# - Keep liveness simple: the process can be alive even when it should stop taking work.
# - Alert before balance reaches zero so queues can drain cleanly.
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()
JavaScript: endpoints de salud con Express
Si tu worker corre sobre Node.js, la lógica es idéntica: un objeto health con contadores y tres rutas Express. Fíjate en que checkBalance reutiliza el valor cacheado durante 60 segundos y en que readiness devuelve 503 en cuanto encuentra un problema, para que el balanceador reaccione de inmediato.
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"));
Cómo conectar los probes en Kubernetes
Kubernetes usa el liveness probe para decidir cuándo reiniciar el contenedor y el readiness probe para decidir si lo incluye en el Service. Dale a liveness un initialDelaySeconds holgado para que el worker arranque sin reinicios prematuros, y a readiness un periodSeconds más corto para que reaccione rápido cuando el saldo baja o los fallos se acumulan.
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
Qué significa cada código de respuesta
Cada endpoint devuelve solo dos estados —200 cuando todo va bien y 503 cuando el orquestador debe actuar— para que las probes sean fáciles de interpretar:
| Endpoint | 200 | 503 |
|---|---|---|
/health/live |
Proceso responsivo | Proceso congelado: reiniciar |
/health/ready |
Puede aceptar trabajo | Deja de enviar tareas |
/health/dependencies |
Dependencias OK | Upstream degradado |
Problemas comunes y cómo resolverlos
La mayoría de los fallos de health checks no vienen de la lógica de resolución, sino de umbrales mal calibrados o de llamadas externas dentro del probe. Esta tabla reúne los casos que más soporte generan:
| Problema | Causa | Solución |
|---|---|---|
| El worker se reinicia sin parar | Umbral de liveness demasiado estricto | Sube failureThreshold o periodSeconds |
Se marca not_ready al arrancar |
Aún no hubo ninguna resolución y se cuenta como "demasiado tiempo" | Evalúa seconds_since_last_solve solo después de la primera resolución |
| El saldo ralentiza el endpoint | Llamada a la API en cada solicitud | Cachea el saldo con un TTL (60 s recomendado) |
| El propio endpoint de salud falla | Excepción sin controlar en un check | Envuelve cada check en try/except y devuelve degraded en vez de 500 |
| Falsos negativos en dependencias | Corte de red puntual al consultar el saldo | Usa valores cacheados con un enfoque stale-while-revalidate |
Preguntas frecuentes
¿Cuál es la diferencia entre liveness y readiness?
Liveness responde "¿el proceso está vivo?" y su fallo reinicia el contenedor. Readiness responde "¿puede aceptar trabajo ahora mismo?" y su fallo solo deja de enrutar tráfico, sin matar el proceso. Un worker sin saldo está vivo pero no listo: liveness debe seguir en 200 y readiness pasar a 503.
¿Qué debe devolver el health check cuando el saldo está bajo?
El readiness debe devolver 503 con el detalle en issues (por ejemplo low_balance), no el liveness. Así el worker sale de rotación y la cola drena hacia otros workers mientras recargas saldo, sin provocar un ciclo de reinicios. Recuerda que CaptchaAI factura por thread concurrente, así que el saldo se agota por consumo, no por número de solicitudes.
¿Puedo usar estos endpoints sin Kubernetes, solo con Docker Compose?
Sí. Cualquier balanceador o supervisor que sepa hacer una petición HTTP sirve: Docker Compose con healthcheck, un nginx con upstream checks, o incluso un script de systemd. Los mismos /health/live y /health/ready funcionan igual; solo cambia quién los consulta.
¿Es seguro exponer /health a Internet?
Mejor no. Los endpoints revelan saldo, tasa de éxito y estado interno, así que exponlos solo en la red interna o detrás de autenticación. Kubernetes y el balanceador los consultan desde dentro del clúster, por lo que no necesitan salir a la red pública.
Artículos relacionados
Siguientes pasos
Deja tus workers de CAPTCHA listos para producción: obtén tu clave API de CaptchaAI y añade estos endpoints de health check a cada instancia.
Guías relacionadas:
- Patrón circuit breaker para llamadas a la API de CAPTCHA
- Patrón bulkhead para aislar la resolución de CAPTCHA
- Monitoreo de tasas de resolución de CAPTCHA con Prometheus y Grafana