Tutorials

Dados de série temporal para tendências de desempenho de resolução de CAPTCHA

Um painel em tempo real diz se a resolução funciona agora. Ele não mostra que a taxa de sucesso caiu de 96% para 88% em três semanas. Quem enxerga isso é a série temporal: cada tentativa vira um ponto com carimbo de tempo, e as perguntas passam a ser feitas em janelas — última hora, últimos sete dias, mesma faixa de horário da semana anterior.

Abaixo, um cliente da API da CaptchaAI instrumentado em Python e Node.js, com as consultas que transformam tendência em decisão.

Por que a métrica pontual esconde a degradação

Três problemas só aparecem quando você compara janelas:

  • Deriva lenta. Dois pontos percentuais por semana somem no dia a dia e saltam no gráfico de 30 dias.
  • Sazonalidade. Um monitoramento autorizado de preços concentra volume no horário comercial; comparar terça às 14h com domingo às 3h não diz nada.
  • Token que expira antes do uso. Quando a fila entre a resolução e o envio do formulário cresce, o solver reporta sucesso e a aplicação reporta rejeição.

As séries temporais que valem o custo de armazenamento

Métrica Tipo Para que serve
Taxa de resolução (%) Gauge Detectar mudanças de qualidade
Latência de resolução (ms) Histogram Dimensionar timeouts
Erros por código Counter Ver padrões surgindo
Custo por resolução (US$) Gauge Orçamento e anomalias
Profundidade da fila Gauge Planejar capacidade
Tokens expirados antes do uso Counter Ajuste de TTL
Saldo da conta Gauge Disparar recarga

O custo por resolução não vem da API: a CaptchaAI cobra por thread simultânea, com resoluções ilimitadas por thread no mês. Divida a mensalidade pelo volume do período — BASIC custa US$ 15/mês com 5 threads; ADVANCE, US$ 90/mês com 50 threads — e grave o resultado como gauge diário. A constante do exemplo é só um marcador.

Escolha o banco de dados de série temporal antes de instrumentar

Critério Prometheus InfluxDB TimescaleDB
Indicado para Monitoramento operacional Alta cardinalidade, IoT Análise em SQL
Consulta PromQL Flux SQL
Retenção Por configuração Por política Do PostgreSQL
Grafana Nativo Nativo Nativo
Curva de aprendizado Baixa Média Baixa com SQL
Auto-hospedado Sim Sim Extensão do PostgreSQL

Se a equipe já roda Prometheus na infraestrutura, use-o. Se as métricas vão cruzar com tabelas de produto ou de QA, TimescaleDB paga o esforço.

Prometheus + Python com push gateway

Workers de resolução costumam ser processos curtos, e o scrape do Prometheus não alcança um processo assim. As métricas vão por push gateway.

Instrumente o worker de resolução

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

O push nunca derruba a resolução: o try/except silencioso é intencional. Os buckets acompanham as faixas reais — o Cloudflare Turnstile costuma ser resolvido em menos de 10 s e o reCAPTCHA v2 pode levar até 60 s.

Consultas PromQL do dia a dia

Estas quatro consultas cobrem quase toda revisão semanal.

# 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])

InfluxDB + Python com armazenamento independente

Grave cada tentativa como um ponto

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)

Cada ponto leva type e status como tags. Manter success como campo numérico simplifica a média móvel: a taxa de sucesso vira a média do campo na janela.

Janelas de 24 horas em Flux

// 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")

Node.js: exponha /metrics no próprio serviço

Em um serviço de longa duração, o push gateway é desnecessário — publique /metrics e deixe o Prometheus fazer o scrape.

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);

Retenção, cardinalidade e LGPD

  • Escalone a retenção. Por segundo em 7 dias, por hora em 90 dias, resumos diários indefinidamente.
  • Limite os rótulos a type, status e error_code. A URL da página como rótulo multiplica a base por milhares em uma semana.
  • Não coloque dado pessoal em rótulo. URLs completas e identificadores de conta transformam o armazenamento de métricas em base de dados pessoais sob a LGPD (RGPD, em Portugal).
  • Rotule a região. Workers em sa-east-1 (São Paulo) e em us-east-1 medem RTT diferente para o mesmo tipo de CAPTCHA.

De tendência a alerta

  1. Colete duas semanas de linha de base antes de definir qualquer limiar.
  2. Alerte sobre a média móvel de 1 hora comparada à mesma faixa de horário das semanas anteriores, nunca sobre pontos isolados.
  3. Separe a latência por tipo: misturar image/OCR com reCAPTCHA v2 na mesma série gera um P95 sem significado.
  4. Trate fila e saldo como sinais de capacidade: fila crescendo com latência estável costuma significar threads de menos.

Problemas comuns no pipeline de métricas de CAPTCHA

Problema Causa Correção
Buracos no gráfico O push gateway não recebeu dados Verifique a rede entre worker e gateway
Percentis sem sentido Buckets fora da carga real Use [5, 10, 15, 20, 30, 45, 60, 90, 120]
Custo medido ≠ gasto real Valor por resolução fixo no código Derive do plano e do volume do mês
Cardinalidade explodindo Rótulos com valores demais Restrinja a type, status, error_code

Perguntas frequentes

Preciso de série temporal se já tenho logs estruturados?

Para saber quanto durou a última resolução, o log basta. Para comparar o P95 desta semana com o da anterior, não: a consulta varreria gigabytes. Os dois convivem — log para investigar um caso, série temporal para ler a tendência.

Como calculo o custo por resolução se a cobrança é por thread?

Divida a mensalidade do plano pelas resoluções concluídas no período. Como as resoluções por thread são ilimitadas no mês, o custo unitário cai conforme o volume sobe — quando a curva estabiliza no fundo, as threads contratadas estão bem aproveitadas.

O tempo medido deve incluir o polling?

Sim, se a métrica serve para dimensionar timeouts. Meça do envio a in.php até o token chegar em res.php — é o tempo que o seu código espera de fato. Registre também as consultas por resolução: a diferença revela polling mal ajustado.

Devo separar as séries por tipo de CAPTCHA?

Sim. As faixas de tempo variam muito entre reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3 e desafios de imagem, e uma série única esconde a degradação de um tipo atrás da média dos outros. Não crie séries para hCaptcha nem FunCaptcha, que não são suportados, nem para GeeTest v4, ainda em breve; CaptchaFox (beta), Friendly Captcha (beta) e Lemin (beta) pedem rótulo próprio.

Próximos passos

Instrumente uma vez e deixe o histórico trabalhar: em duas semanas o gráfico responde o que nenhum painel instantâneo responde. Crie sua chave de API da CaptchaAI e grave a primeira série.

Guias relacionados:

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