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,statuseerror_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 emus-east-1medem RTT diferente para o mesmo tipo de CAPTCHA.
De tendência a alerta
- Colete duas semanas de linha de base antes de definir qualquer limiar.
- Alerte sobre a média móvel de 1 hora comparada à mesma faixa de horário das semanas anteriores, nunca sobre pontos isolados.
- Separe a latência por tipo: misturar image/OCR com reCAPTCHA v2 na mesma série gera um P95 sem significado.
- 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: