Tutorials

Data Time-Series untuk Tren Performa Solve CAPTCHA

Pertanyaan yang biasanya muncul jam dua pagi bukan "berapa tingkat keberhasilan saya sekarang", melainkan "sejak kapan angkanya turun". Dashboard yang hanya menampilkan nilai saat ini tidak bisa menjawabnya; yang bisa menjawab adalah deret metrik yang disimpan dengan stempel waktu. Artikel ini menunjukkan metrik solve CAPTCHA mana yang layak direkam, cara mengirimnya ke Prometheus atau InfluxDB dari worker Python dan Node.js, serta ambang alert yang realistis berdasarkan angka SLA per tipe CAPTCHA.

Instrumentasi ini bersifat aditif: alur kirim → simpan task ID → polling → pakai token tetap sama, hanya kini setiap langkah meninggalkan jejak angka.

Metrik solve CAPTCHA yang wajib direkam

Tujuh deret berikut cukup untuk mendeteksi hampir semua degradasi nyata. Nama tipe metrik dibiarkan dalam bahasa aslinya karena itulah nama kelas di kode.

Metrik Tipe Kenapa penting
Tingkat keberhasilan (%) Gauge Sinyal paling langsung kalau kualitas penyelesaian bergeser
Latensi penyelesaian (ms) Histogram Dasar untuk menentukan batas waktu dan mendeteksi perlambatan
Jumlah error per kode Counter Memisahkan ERROR_ZERO_BALANCE dari CAPCHA_NOT_READY yang wajar
Biaya efektif per solve ($) Gauge Pelacakan anggaran dan deteksi lonjakan volume
Kedalaman antrean Gauge Perencanaan kapasitas thread
Token kedaluwarsa sebelum dipakai Counter Sinyal bahwa TTL atau alur checkout perlu disetel
Saldo akun API Gauge Pemicu isi ulang sebelum worker berhenti

Dua yang paling sering dilupakan justru yang paling mahal: token yang kedaluwarsa sebelum sempat dikirim ke form, dan saldo yang habis di tengah batch malam hari.

Memilih database time-series sebelum menulis kode

Keputusan ini menentukan bahasa query yang akan Anda pakai seterusnya, jadi tetapkan di awal.

Aspek Prometheus InfluxDB TimescaleDB
Paling cocok untuk Monitoring operasional Metrik berkardinalitas tinggi Analisis berbasis SQL
Bahasa query PromQL Flux SQL
Retensi Berbasis konfigurasi Berbasis kebijakan Mengikuti PostgreSQL
Integrasi Grafana Native Native Native
Kurva belajar Rendah Sedang Rendah jika Anda sudah paham SQL
Self-hosted Ya Ya Ya (ekstensi PostgreSQL)

Aturan praktisnya: kalau tim Anda sudah menjalankan Prometheus untuk infrastruktur, tambahkan saja metrik CAPTCHA ke instance yang ada. InfluxDB masuk akal kalau Anda ingin penyimpanan metrik terpisah dari monitoring infra. TimescaleDB menang kalau analisis biaya bulanan Anda memang sudah hidup di PostgreSQL.

Instrumentasi worker Python dengan Prometheus

Worker solver umumnya berumur pendek: jalan, selesaikan batch, mati. Karena itu polanya memakai push gateway, bukan scrape endpoint.

Menambahkan metrik ke fungsi solve

Perhatikan push_metrics() yang menelan exception: kegagalan mengirim metrik tidak boleh membuat penyelesaian CAPTCHA ikut gagal. Perhatikan juga konstanta biaya di SOLVE_COST — ganti dengan biaya efektif Anda sendiri, karena CaptchaAI menagih per thread, bukan per solve.

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

Query PromQL untuk tren harian

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

Empat query ini cukup untuk satu baris panel Grafana.

Mengirim metrik penyelesaian ke InfluxDB

Di InfluxDB model datanya berubah dari deret bernama menjadi measurement bertag: simpan type dan status sebagai tag, angka mentah sebagai field.

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)

Query Flux per jendela satu jam

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

Agregasi per jam adalah resolusi paling berguna: cukup halus untuk melihat insiden, cukup kasar agar grafik 30 hari tetap terbaca.

Versi Node.js dengan prom-client

Kalau pipeline scraping Anda berjalan di Node.js, polanya identik dengan berganti library. Worker di sini mengekspos endpoint /metrics sendiri, jadi Prometheus bisa scrape langsung tanpa 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);

Contoh nyata: tim scraping tiga orang di Jakarta

Bayangkan tim monitoring harga beranggotakan tiga orang yang menjalankan worker di AWS ap-southeast-3 (Jakarta) dan berlangganan paket ADVANCE ($90/bulan, 50 thread, solve tanpa batas per thread). Karena penagihan berbasis thread, biaya per penyelesaian bukan angka tetap dari vendor melainkan hasil bagi: $90 dibagi jumlah solve yang benar-benar diselesaikan bulan itu. Bulan dengan 300.000 solve menghasilkan biaya efektif yang jauh lebih rendah dibanding bulan dengan 40.000 solve, dengan tagihan yang sama persis.

Di situlah data time-series membayar dirinya sendiri. Grafik increase(captcha_solve_total[24h]) di samping garis biaya bulanan yang datar memperlihatkan kapan paket mulai kekurangan thread: antrean memanjang, latensi naik, tagihan tidak bergerak. Itu sinyal menaikkan paket, bukan menambah percobaan ulang. Sebaliknya, bila kedalaman antrean nyaris selalu nol, paket STANDARD ($30/bulan, 15 thread) mungkin sudah memadai.

Satu catatan kepatuhan: rekam metrik operasional (waktu, status, kode error), bukan konten halaman yang diproses — UU 27/2022 tentang Pelindungan Data Pribadi membuat penyimpanan data pribadi di dalam metrik jadi risiko yang tak perlu.

Masalah umum dan cara memperbaikinya

Masalah Penyebab Perbaikan
Ada lubang kosong di grafik Push gateway tidak menerima data dari worker Periksa jalur jaringan antara worker dan push gateway, lalu cek log push_to_gateway
Persentil latensi terlihat aneh Batas bucket tidak cocok dengan beban kerja Pakai [5, 10, 15, 20, 30, 45, 60, 90, 120] yang sudah disesuaikan untuk penyelesaian CAPTCHA
Angka biaya tidak cocok dengan tagihan Konstanta biaya per solve masih nilai contoh Hitung ulang dari harga paket bulanan dibagi volume solve bulan berjalan
Kardinalitas meledak Terlalu banyak nilai label unik Batasi label pada type, status, dan error_code; jangan pernah memberi tag berisi URL
Latensi naik hanya untuk satu tipe Beban di tipe tertentu, bukan masalah global Pisahkan grafik per type sebelum menyimpulkan ada gangguan menyeluruh

Pertanyaan yang sering muncul

Bagaimana menghitung biaya per solve kalau CaptchaAI menagih per thread?

Bagi harga paket bulanan dengan jumlah solve sukses pada bulan yang sama. Paket berbasis thread — BASIC ($15/bulan, 5 thread) sampai VIP-3 ($7,500/bulan, 5.000 thread) — memberi solve tanpa batas per thread, sehingga biaya efektif turun seiring naiknya volume. Rekam pembaginya sebagai deret time-series agar terlihat sebagai tren.

Ambang alert berapa yang masuk akal untuk latensi?

Turunkan dari angka SLA per tipe, bukan dari satu angka global. reCAPTCHA v2 diselesaikan dalam waktu di bawah 60 detik, Cloudflare Turnstile di bawah 10 detik, dan GeeTest v3 di bawah 12 detik. Pasang alert P95 sedikit di atas angka itu untuk masing-masing tipe, lalu evaluasi ulang ambangnya setelah dua minggu data terkumpul. Simpan ambang tiap tipe di konfigurasi alert, bukan di kepala Anda.

Apakah worker di region Jakarta membuat latensi terlihat lebih tinggi?

Sebagian kecil, ya — waktu yang Anda ukur mencakup round-trip jaringan dari worker ke API, jadi jarak region ikut terhitung. Cara membedakannya: catat juga durasi request in.php saja. Kalau yang membengkak waktu polling dan bukan waktu submit, penyebabnya antrean penyelesaian, bukan jaringan.

Bagaimana mencegah ledakan kardinalitas label?

Jangan pernah memasukkan nilai yang tidak terbatas jumlahnya ke dalam label: URL halaman, sitekey, task ID, atau ID pengguna. Cukup type, status, dan error_code yang jumlah nilainya bisa dihitung dengan jari. Detail per task simpan di log atau database analitik, bukan di deret metrik.

Langkah berikutnya

Pasang instrumentasinya, biarkan data terkumpul dua minggu, lalu tentukan ambang alert dari angka Anda sendiri — ambil API key CaptchaAI dan mulai rekam metrik pertama.

Bacaan lanjutan:

Komentar dinonaktifkan untuk artikel ini.