Tutoriales

Series temporales para analizar el rendimiento al resolver CAPTCHA

Cuando tu tasa de resolución cae un martes por la tarde, un panel del minuto actual no sirve: dice que algo va mal, pero no desde cuándo. La respuesta es guardar cada resolución como un punto con marca de tiempo —tipo de CAPTCHA, estado, duración y costo— en una base de datos de series temporales. Así comparas semanas, correlacionas una subida de latencia con un despliegue y alertas antes de que la cola se acumule.

Verás qué registrar, cómo instrumentar el solver en Python y Node.js y qué consultas dan las tendencias útiles, sobre los endpoints in.php y res.php de CaptchaAI.

Qué métricas registrar y de qué tipo

Decide primero qué mides: contador, gauge e histograma responden preguntas distintas, y elegir mal deja la métrica inservible meses después.

Métrica Tipo Para qué
Tasa de resolución (%) Gauge Cambios de calidad
Latencia (ms) Histograma Ralentizaciones, timeouts
Errores por código Counter Fallos emergentes
Costo efectivo ($) Gauge Presupuesto
Profundidad de la cola Gauge Capacidad
Tokens caducados sin usar Counter Ajuste del TTL

Sobre el costo: CaptchaAI factura por thread concurrente, no por resolución, con resoluciones ilimitadas en cada plan. BASIC ($15/mes, 5 threads) o ADVANCE ($90/mes, 50 threads) cuestan igual resuelvas mil o cien mil CAPTCHA. La serie de costo mide eficiencia —cuota mensual entre volumen real—, no facturación.

Escenario: monitoreo de un portal de citas previas

Caso habitual en el mercado hispanohablante: una agencia con clientes en España y México comprueba la disponibilidad en portales públicos de trámites —cita previa, visados tipo BLS CAPTCHA, consultas fiscales— en flujos autorizados. Esos portales cambian su configuración de CAPTCHA sin avisar y de madrugada.

Sin series temporales el equipo se entera cuando un cliente reclama; con ellas, la latencia P95 por tipo muestra el escalón a las 03:00 y revela que solo se degradó un tipo. Respeta los términos de servicio del portal y la normativa de protección de datos aplicable (GDPR/LOPDGDD, LFPDPPP).

Instrumentar el solver en Python con Prometheus

Prometheus hace scraping de un endpoint HTTP, pero un worker de CAPTCHA suele ser efímero, sin nada estable que rastrear. Para eso está el Push Gateway: tu proceso empuja las métricas y Prometheus consulta ahí.

Instrumenta tu worker de resolución

El patrón envuelve la llamada a la API: marcas el tiempo al enviar la tarea a in.php, incrementas el contador según el desenlace y observas la duración cuando res.php devuelve el token. push_metrics() traga las excepciones a propósito: un fallo del gateway no debe tumbar una resolución válida.

import os
import time
import requests
from prometheus_client import CollectorRegistry, Counter, Histogram, Gauge, push_to_gateway

registry = CollectorRegistry()

SOLVE_TOTAL = Counter(
    "captcha_solve_total", "Total CAPTCHA solve attempts",
    ["type", "status"], registry=registry
)
SOLVE_LATENCY = Histogram(
    "captcha_solve_latency_seconds", "CAPTCHA solve latency",
    ["type"], buckets=[5, 10, 15, 20, 30, 45, 60, 90, 120],
    registry=registry
)
SOLVE_COST = Counter(
    "captcha_solve_cost_dollars", "Total cost of CAPTCHA solves",
    ["type"], registry=registry
)
API_BALANCE = Gauge(
    "captcha_api_balance_dollars", "CaptchaAI account balance",
    registry=registry
)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
PUSHGATEWAY = os.environ.get("PUSHGATEWAY_URL", "localhost:9091")


def solve_with_metrics(sitekey, pageurl, captcha_type="recaptcha_v2"):
    start = time.time()

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1
    })
    data = resp.json()

    if data.get("status") != 1:
        SOLVE_TOTAL.labels(type=captcha_type, status="submit_error").inc()
        push_metrics()
        return {"error": data.get("request")}

    captcha_id = data["request"]

    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get",
            "id": captcha_id, "json": 1
        }).json()

        if result.get("status") == 1:
            elapsed = time.time() - start
            SOLVE_TOTAL.labels(type=captcha_type, status="solved").inc()
            SOLVE_LATENCY.labels(type=captcha_type).observe(elapsed)
            SOLVE_COST.labels(type=captcha_type).inc(0.00299)
            push_metrics()
            return {"solution": result["request"]}

        if result.get("request") != "CAPCHA_NOT_READY":
            SOLVE_TOTAL.labels(type=captcha_type, status="error").inc()
            push_metrics()
            return {"error": result.get("request")}

    SOLVE_TOTAL.labels(type=captcha_type, status="timeout").inc()
    push_metrics()
    return {"error": "TIMEOUT"}


def push_metrics():
    try:
        push_to_gateway(PUSHGATEWAY, job="captcha_solver", registry=registry)
    except Exception:
        pass  # Don't fail solving because metrics push failed


def update_balance():
    resp = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "getbalance"
    })
    try:
        balance = float(resp.text)
        API_BALANCE.set(balance)
        push_metrics()
    except ValueError:
        pass

Los cubos del histograma ([5, 10, 15, 20, 30, 45, 60, 90, 120]) están en segundos. Si copias los cubos por defecto de una librería HTTP, todas las muestras caerán en el último y los percentiles no dirán nada.

Consultas PromQL para ver la tendencia

Cuatro consultas cubren el día a día: tasa de éxito, P95 de latencia, errores por tipo y costo por hora.

# Success rate over last hour
rate(captcha_solve_total{status="solved"}[1h])
/ rate(captcha_solve_total[1h]) * 100

# P95 solve latency
histogram_quantile(0.95, rate(captcha_solve_latency_seconds_bucket[1h]))

# Error rate by type
rate(captcha_solve_total{status="error"}[1h])

# Hourly cost
increase(captcha_solve_cost_dollars_total[1h])

El P95 debe ir en el panel principal. La media miente: si el 90 % tarda 8 s y el resto 70 s, parece sana mientras una décima parte espera más de un minuto.

Guardar las mismas métricas en InfluxDB

Si prefieres un almacén independiente de la infraestructura, InfluxDB encaja mejor. El modelo cambia: escribes puntos con tags (dimensiones indexadas) y fields (valores numéricos).

Escribir cada resolución como punto

from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS

INFLUX_URL = os.environ.get("INFLUX_URL", "http://localhost:8086")
INFLUX_TOKEN = os.environ.get("INFLUX_TOKEN", "")
INFLUX_ORG = os.environ.get("INFLUX_ORG", "captcha")
INFLUX_BUCKET = os.environ.get("INFLUX_BUCKET", "captcha_metrics")

influx_client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = influx_client.write_api(write_options=SYNCHRONOUS)


def record_solve_metric(captcha_type, status, elapsed_ms, cost=0.0, error=None):
    point = (
        Point("captcha_solve")
        .tag("type", captcha_type)
        .tag("status", status)
        .field("elapsed_ms", elapsed_ms)
        .field("cost", cost)
        .field("success", 1 if status == "solved" else 0)
    )
    if error:
        point = point.tag("error_code", error)
    write_api.write(bucket=INFLUX_BUCKET, record=point)


def record_balance(balance):
    point = Point("captcha_balance").field("balance", balance)
    write_api.write(bucket=INFLUX_BUCKET, record=point)

Regla práctica: type, status y error_code sí como tags; el id de la tarea o la URL, nunca. Cada combinación crea una serie, y una URL genera millones que acabarán con la memoria del servidor.

Consultas Flux: tasa de éxito, latencia y costo

Estas tres reproducen en Flux lo mismo que en PromQL, en ventanas de una hora sobre las últimas 24.

// Success rate over last 24 hours (1-hour windows)
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "success")
  |> aggregateWindow(every: 1h, fn: mean)
  |> map(fn: (r) => ({r with _value: r._value * 100.0}))
  |> yield(name: "success_rate")

// Average solve time by type
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "elapsed_ms" and r.status == "solved")
  |> group(columns: ["type"])
  |> aggregateWindow(every: 1h, fn: mean)
  |> yield(name: "avg_latency")

// Cumulative cost
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "cost")
  |> cumulativeSum()
  |> yield(name: "cumulative_cost")

aggregateWindow es la pieza clave: convierte miles de puntos sueltos en una serie por horas, la resolución a la que se leen las tendencias. Para informes semanales, every: 1d.

La misma instrumentación en Node.js

En JavaScript, prom-client da los mismos tipos de métrica, pero un servicio Node es de larga vida: expones /metrics directamente y te olvidas del Push Gateway.

const client = require("prom-client");
const axios = require("axios");

const register = new client.Registry();
const API_KEY = process.env.CAPTCHAAI_API_KEY;

const solveTotal = new client.Counter({
  name: "captcha_solve_total",
  help: "Total CAPTCHA solve attempts",
  labelNames: ["type", "status"],
  registers: [register],
});

const solveLatency = new client.Histogram({
  name: "captcha_solve_latency_seconds",
  help: "CAPTCHA solve latency",
  labelNames: ["type"],
  buckets: [5, 10, 15, 20, 30, 45, 60, 90, 120],
  registers: [register],
});

async function solveWithMetrics(sitekey, pageurl, type = "recaptcha_v2") {
  const start = Date.now();

  const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
    params: { key: API_KEY, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
  });

  if (submit.data.status !== 1) {
    solveTotal.inc({ type, status: "submit_error" });
    return { error: submit.data.request };
  }

  const captchaId = submit.data.request;

  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const poll = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
    });

    if (poll.data.status === 1) {
      const elapsed = (Date.now() - start) / 1000;
      solveTotal.inc({ type, status: "solved" });
      solveLatency.observe({ type }, elapsed);
      return { solution: poll.data.request };
    }

    if (poll.data.request !== "CAPCHA_NOT_READY") {
      solveTotal.inc({ type, status: "error" });
      return { error: poll.data.request };
    }
  }

  solveTotal.inc({ type, status: "timeout" });
  return { error: "TIMEOUT" };
}

// Expose metrics endpoint
const express = require("express");
const app = express();
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});
app.listen(9090);

Detalle que se pasa por alto: con varias réplicas, cada una expone sus métricas. Prometheus las agrega bien porque cada instancia lleva su etiqueta, pero un panel que sume sin agrupar infla los números.

Qué base de datos elegir

Característica Prometheus InfluxDB TimescaleDB
Mejor para Monitoreo operativo Alta cardinalidad Análisis con SQL
Consultas PromQL Flux SQL
Retención Configuración Políticas PostgreSQL
Grafana Nativa Nativa Nativa
Aprendizaje Bajo Medio Bajo con SQL

La decisión rara vez es técnica: si ya operas Prometheus, añadir las métricas de CAPTCHA cuesta una tarde, y montar un segundo sistema solo para el solver casi nunca se justifica.

Problemas frecuentes al montar el pipeline de métricas

Síntoma Causa Solución
Huecos en las gráficas El Push Gateway no recibe datos Revisa la red worker–gateway
Percentiles absurdos Cubos mal ajustados Cubos en segundos, no en ms
El costo no cuadra Se modela como pago por resolución Cobro por thread: cuota entre volumen
El servidor se queda sin memoria Cardinalidad descontrolada Tags: solo type, status, error_code

Preguntas frecuentes

¿Cada cuánto debo empujar las métricas al Push Gateway?

Al cerrar cada resolución, como arriba: un push por CAPTCHA es poco volumen y nada se pierde si el worker muere. Por encima de varios cientos por minuto, acumula en memoria y empuja cada 10–15 segundos.

¿Qué umbral de alerta tiene sentido para la tasa de resolución?

El que salga de tu línea base, no una cifra universal. Mide dos semanas, calcula la media móvil de 1 hora y alerta si cae de forma sostenida por debajo.

¿Sirven para comparar tipos de CAPTCHA entre sí?

Sí. Etiquetando por type verás que reCAPTCHA v2, Cloudflare Turnstile y GeeTest v3 tienen perfiles de latencia distintos, y podrás fijar tiempos de espera por tipo.

¿Puedo medir tipos en beta como CaptchaFox o Lemin?

Sí, pero con cautela: CaptchaFox (beta), Friendly Captcha (beta) y Lemin (beta) siguen en beta, así que sus tendencias valen para tu entorno y no como referencia estable. hCaptcha, FunCaptcha y GeeTest v4 no están en el catálogo de CaptchaAI y no aparecerán en tus series.

Empieza a medir hoy

Instrumentar el solver es media hora de trabajo que ahorra semanas de diagnóstico a ciegas. Obtén tu clave API de CaptchaAI, añade el histograma y el contador a tu worker y deja correr una semana: la primera gráfica dirá lo que el panel en vivo nunca contó.

Guías relacionadas:

Los comentarios están deshabilitados para este artículo.