Tutorials

Métriques de résolution CAPTCHA en séries temporelles

Un taux de réussite affiché en direct ne répond jamais à la question qui compte un lundi matin : est-ce que ça se dégrade ? Historisez chaque résolution CAPTCHA — horodatage, type, durée, statut, coût amorti — puis lisez la pente plutôt que la valeur instantanée. Ce guide instrumente votre solveur en Python et en Node.js, écrit ces mesures dans Prometheus ou InfluxDB, et donne les requêtes qui révèlent une dérive avant que les files ne débordent.

Choisir votre base de séries temporelles

La décision de stockage vient en premier : elle fixe le langage de requête que vos équipes écriront.

Critère Prometheus InfluxDB TimescaleDB
Terrain de prédilection Supervision opérationnelle Métriques à forte cardinalité Analyse en SQL
Langage de requête PromQL Flux SQL
Rétention Fixée en configuration Pilotée par politiques Héritée de PostgreSQL
Prise en main Rapide Moyenne Immédiate en SQL
Auto-hébergement Oui Oui Oui (extension PostgreSQL)

Si Prometheus supervise déjà vos workers, ajoutez-y les mesures CAPTCHA plutôt que de monter une seconde pile. InfluxDB se justifie quand ce suivi vit hors de votre supervision, TimescaleDB quand vos analystes croisent les résolutions avec des tables métier.

Les sept mesures à historiser

Mesure Type Ce qu'elle révèle
Taux de réussite (%) Jauge Un changement de qualité côté fournisseur
Temps de résolution (ms) Histogramme Les ralentissements, le réglage des timeouts
Erreurs par code Compteur Un motif d'erreur émergent
Coût amorti par résolution ($) Jauge Le suivi budgétaire, les anomalies de volume
Profondeur de la file d'attente Jauge Un besoin de threads supplémentaires
Tokens expirés avant usage Compteur Un TTL mal calibré côté injection
Solde du compte API Jauge Le seuil de recharge

Deux précautions. La cardinalité : limitez les étiquettes à type, status et error_code, jamais l'URL cible. Le RGPD : aucune URL porteuse d'un identifiant utilisateur ne doit finir dans une étiquette conservée 90 jours.

Instrumenter votre solveur Python avec Prometheus

Le solveur envoie la tâche, interroge le résultat, puis pousse ses compteurs vers une Push Gateway — indispensable pour des workers éphémères que Prometheus ne peut pas scraper. Le try/except autour du push est volontaire : une supervision indisponible ne doit jamais faire échouer une résolution.

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

Les requêtes PromQL à garder sous la main

Quatre requêtes couvrent l'essentiel : taux de réussite glissant, temps de résolution P95, erreurs par type, coût de l'heure écoulée.

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

Historiser les mêmes mesures dans InfluxDB

Le modèle change : vous écrivez des points bruts, tags pour les axes d'analyse, champs pour les valeurs. L'agrégation se fait à la lecture, ce qui laisse ouverts des découpages imprévus.

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)

Trois requêtes Flux pour lire la tendance

Fenêtres d'une heure sur 24 heures pour le taux de réussite, temps de résolution moyen par type, somme cumulée du coût.

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

Le même suivi côté Node.js

Sous Node.js, prom-client expose les mêmes compteurs et le même histogramme, via un endpoint /metrics scrapé par Prometheus.

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

Un exemple concret : un pipeline hébergé à Paris

Une équipe parisienne déploie ses workers sur trois instances OVHcloud, derrière un Prometheus unique. Elle résout environ 40 000 CAPTCHA par mois, surtout du reCAPTCHA v2 et du Cloudflare Turnstile, sur un plan ADVANCE ($90/mois, 50 threads). Pendant six semaines la courbe reste plate, puis le P95 du temps de résolution passe de 18 à 34 secondes en trois jours, taux de réussite inchangé : ce n'est pas une panne, c'est la file d'attente qui s'allonge parce que le trafic a doublé.

Sans historique, l'équipe l'aurait découvert le jour où les timeouts coupaient les tâches. Ici la décision est arithmétique : les threads saturent, on passe à PREMIUM ($170/mois, 100 threads) avant l'incident. Ces chiffres reposent sur des mesures observées ; les résultats varient selon l'environnement et le volume.

Rétention et coût réel par résolution

Trois paliers couvrent tous les usages : la seconde pendant 7 jours pour l'analyse d'incident, l'heure pendant 90 jours pour la tendance, un résumé quotidien sans limite. La rétention fine coûte peu ; c'est la cardinalité qui pèse.

Le compteur de coût mérite une explication, car la facturation CaptchaAI est basée sur les threads, pas sur le nombre de résolutions : chaque plan inclut des threads simultanés et des résolutions illimitées, de BASIC ($15/mois, 5 threads) jusqu'à VIP-3 ($7,500/mois, 5 000 threads). La constante du code n'est donc pas un tarif à l'unité, mais un coût amorti obtenu en divisant le prix mensuel du plan par le volume résolu. Suivi dans le temps, ce ratio devient l'indicateur de dimensionnement le plus parlant : s'il grimpe, vous payez des threads inutilisés. La facturation reste en dollars US.

Calez enfin vos compartiments d'histogramme sur les plafonds annoncés par type — Cloudflare Turnstile sous les 10 s, reCAPTCHA v2 sous les 60 s — et non sur des seuils hérités d'une autre application.

Dépannage

Problème Cause probable Correctif
Trous dans les courbes La Push Gateway ne reçoit plus les workers Vérifiez la route réseau et le retour de push_to_gateway
Centiles de latence incohérents Bornes de compartiments inadaptées Reprenez [5, 10, 15, 20, 30, 45, 60, 90, 120], calibrées pour la résolution CAPTCHA
Coût mesuré différent de la facture Constante figée alors que le plan est facturé au thread Recalculez le coût amorti chaque mois
Solde jamais mis à jour update_balance() n'est planifié nulle part Programmez l'appel toutes les 15 minutes

FAQ

Faut-il un serveur dédié pour Prometheus ou InfluxDB ?

Non. Pour quelques dizaines de milliers de résolutions par mois, une instance mutualisée suffit : le dimensionnement dépend de la cardinalité des étiquettes, pas du volume.

Comment calculer un coût par résolution avec une facturation au thread ?

Divisez le prix mensuel du plan par le nombre de résolutions réussies du mois, et publiez ce ratio comme une jauge recalculée chaque nuit — la seule façon honnête de tracer un coût quand les résolutions sont illimitées.

Quelles alertes mettre en place en premier ?

Deux suffisent : taux de réussite glissant sur 1 heure sous 90 %, et P95 du temps de résolution au-dessus de 45 secondes pendant 15 minutes. Ajoutez ensuite le solde du compte.

Ces mesures contiennent-elles des données personnelles au sens du RGPD ?

Elles ne devraient pas. Un point se limite à un horodatage, un type, un statut et une durée. Vérifiez qu'aucun identifiant de session ne remonte dans les étiquettes avant une rétention longue.

Prochaines étapes

Instrumentez d'abord, décidez ensuite : récupérez votre clé API CaptchaAI et poussez vos premières mesures.

Guides associés :

Les commentaires sont désactivés pour cet article.