Tutorials

Redis untuk Manajemen dan Caching Token CAPTCHA

Cache token CAPTCHA di Redis baru terasa gunanya dalam satu kondisi: ketika beberapa worker menghadapi sitekey yang sama dan meminta penyelesaian identik pada detik yang sama. Redis tidak membuat CAPTCHA selesai lebih cepat; Redis memastikan Anda tidak menghabiskan thread untuk pekerjaan yang sudah selesai satu menit lalu.

Tiga hal menentukan apakah cache token Anda berguna atau justru merepotkan: TTL bermargin aman, kunci dedup untuk solve yang sedang berjalan, dan kumpulan token yang selalu terisi. Polanya tersedia dalam Python dan Node.js.

Kapan cache token layak dipakai, kapan tidak

Umur token menentukan segalanya. Token reCAPTCHA hidup sekitar 90–120 detik, token Cloudflare Turnstile sekitar 300 detik. Jendela itu pendek, jadi cache hanya masuk akal kalau permintaan Anda datang rapat.

Cache layak dipakai jika:

  • Banyak worker menargetkan kombinasi sitekey + pageurl yang sama dalam hitungan detik.
  • Anda menjalankan job batch yang menyentuh halaman berformulir sama berulang kali.
  • Anda ingin latensi nyaris nol pada jam sibuk, bukan menunggu satu siklus solve penuh.

Cache tidak layak dipakai kalau Anda hanya menyelesaikan satu CAPTCHA sesekali atau setiap target punya sitekey berbeda: Redis hanya menambah komponen yang bisa gagal tanpa satu pun cache hit.

Ada sisi biaya yang khas untuk pembaca Indonesia. Banyak tim otomasi di sini — agensi price monitoring, tim data startup, kontraktor scraping lepas — bekerja dengan anggaran ketat dan mesin kecil di AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta). CaptchaAI menagih per thread bersamaan, bukan per solve, dengan solve tanpa batas per thread: BASIC ($15/bulan, 5 thread), STANDARD ($30/bulan, 15 thread), ADVANCE ($90/bulan, 50 thread). Jadi cache tidak memangkas tagihan per solve, melainkan jumlah thread yang Anda butuhkan saat puncak trafik: tim dengan 40 worker yang menabrak halaman login staging yang sama bisa tetap nyaman di paket 15 thread karena kunci dedup mencegah 40 permintaan berubah menjadi 40 solve.

Alur token dari permintaan sampai cache hit

Sebelum menulis kode, pastikan alurnya jelas: cek Redis dulu, baru panggil API kalau memang kosong.

Solve Request → Check Redis → Cache Hit?
                    │               │
                    │ No            │ Yes → Return cached token
                    ▼               
              CaptchaAI API
                    │
                    ▼
              Store in Redis (TTL = token_lifetime - safety_margin)
                    │
                    ▼
              Return token

Detail yang sering terlewat: token harus keluar dari cache sebelum mati, bukan tepat saat mati.

Desain kunci Redis dan TTL bermargin aman

Empat pola kunci sudah cukup untuk hampir semua pipeline:

Pola kunci Fungsi TTL
captcha:token:{sitekey}:{pageurl} Antrean token hasil solve yang siap dipakai 80–250 detik (per tipe)
captcha:lock:{sitekey}:{pageurl} Kunci dedup untuk solve yang sedang berjalan 120 detik
captcha:pool:{sitekey}:{pageurl} Kumpulan token cadangan untuk target sibuk 80–250 detik
captcha:stats:{date} Penghitung solve harian 7 hari

Aturan margin amannya sederhana: potong 30–40 detik dari umur token sebenarnya. Token reCAPTCHA yang hidup sekitar 120 detik disimpan dengan TTL 80 detik, jadi aplikasi Anda masih punya 40 detik untuk mengirimkannya. Turnstile disimpan 250 detik. Margin itu bukan kemewahan: antara token keluar dari Redis dan tiba di form target masih ada request HTTP, antrean worker, dan kadang jaringan seluler yang lambat.

Menyiapkan koneksi Redis dan tabel TTL

Mulai dari konfigurasi koneksi dan satu tabel TTL terpusat. Menyebar angka TTL di banyak fungsi adalah jalan pintas menuju cache yang mengembalikan token mati.

import os
import time
import json
import redis
import requests

r = redis.Redis(
    host=os.environ.get("REDIS_HOST", "localhost"),
    port=int(os.environ.get("REDIS_PORT", 6379)),
    db=0,
    decode_responses=True
)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# TTLs with safety margin (seconds before actual expiration)
TOKEN_TTLS = {
    "recaptcha_v2": 80,     # Actual: ~120s, cache for 80s
    "recaptcha_v3": 80,
    "hcaptcha": 80,
    "turnstile": 250,       # Actual: ~300s, cache for 250s
}

Satu catatan jujur soal tabel di atas: entri hcaptcha ada sebagai bagian dari struktur, tetapi hCaptcha belum didukung CaptchaAI. Untuk tipe itu Anda memerlukan layanan lain.

Operasi dasar: menyimpan dan mengambil token

Dua fungsi ini adalah inti cache. Token diambil dengan lpop supaya satu token tidak pernah dipakai dua worker sekaligus, lalu sisa TTL diperiksa: kalau tinggal di bawah 10 detik, token dianggap tidak layak kirim.

def cache_key(sitekey, pageurl):
    """Generate Redis key for a specific CAPTCHA target."""
    return f"captcha:token:{sitekey}:{pageurl}"


def get_cached_token(sitekey, pageurl):
    """Pop a cached token from the queue."""
    key = cache_key(sitekey, pageurl)
    token = r.lpop(key)
    if token:
        # Verify TTL still valid on the list
        ttl = r.ttl(key)
        if ttl > 10:  # At least 10 seconds remaining
            return token
    return None


def cache_token(sitekey, pageurl, token, captcha_type="recaptcha_v2"):
    """Push a solved token to the cache with appropriate TTL."""
    key = cache_key(sitekey, pageurl)
    ttl = TOKEN_TTLS.get(captcha_type, 80)
    r.rpush(key, token)
    r.expire(key, ttl)

Menyelesaikan CAPTCHA dengan pemeriksaan cache lebih dulu

Alurnya tetap pola empat langkah yang sama: kirim ke in.php, simpan task ID, polling res.php, pakai token. Bedanya ada dua lapisan di depan. Lapisan pertama membaca cache; lapisan kedua memasang kunci dedup dengan SET NX EX, sehingga worker kedua untuk target yang sama menunggu hasil worker pertama alih-alih ikut mengirim permintaan.

def solve_recaptcha(sitekey, pageurl, captcha_type="recaptcha_v2"):
    """Solve reCAPTCHA with Redis cache check."""
    # 1. Check cache
    cached = get_cached_token(sitekey, pageurl)
    if cached:
        return {"solution": cached, "source": "cache"}

    # 2. Check if solve is already in progress (dedup)
    lock_key = f"captcha:lock:{sitekey}:{pageurl}"
    if not r.set(lock_key, "1", nx=True, ex=120):
        # Another worker is solving — wait for result
        for _ in range(60):
            time.sleep(2)
            cached = get_cached_token(sitekey, pageurl)
            if cached:
                return {"solution": cached, "source": "cache_wait"}
        return {"error": "TIMEOUT_WAITING_FOR_OTHER_WORKER"}

    try:
        # 3. Solve via CaptchaAI
        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:
            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:
                token = result["request"]
                cache_token(sitekey, pageurl, token, captcha_type)
                return {"solution": token, "source": "api"}

            if result.get("request") != "CAPCHA_NOT_READY":
                return {"error": result.get("request")}

        return {"error": "TIMEOUT"}
    finally:
        r.delete(lock_key)

Perhatikan blok finally: kunci selalu dilepas, bahkan ketika solve gagal. Tanpa itu, worker yang mati di tengah jalan memblokir target sampai TTL kunci habis.

Menjaga kumpulan token tetap terisi

Untuk target dengan throughput tinggi, menunggu satu siklus solve tetap terasa lambat. Jalankan thread latar yang terus mengisi ulang kumpulan token, sehingga worker tinggal mengambil dari antrean.

import threading


class TokenPool:
    def __init__(self, sitekey, pageurl, pool_size=5, captcha_type="recaptcha_v2"):
        self.sitekey = sitekey
        self.pageurl = pageurl
        self.pool_size = pool_size
        self.captcha_type = captcha_type
        self.pool_key = f"captcha:pool:{sitekey}:{pageurl}"
        self._running = False

    def start(self):
        self._running = True
        thread = threading.Thread(target=self._refill_loop, daemon=True)
        thread.start()

    def stop(self):
        self._running = False

    def _refill_loop(self):
        while self._running:
            current = r.llen(self.pool_key)
            if current < self.pool_size:
                self._solve_and_add()
            time.sleep(2)

    def _solve_and_add(self):
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": API_KEY,
            "method": "userrecaptcha",
            "googlekey": self.sitekey,
            "pageurl": self.pageurl,
            "json": 1
        })
        data = resp.json()
        if data.get("status") != 1:
            return

        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:
                ttl = TOKEN_TTLS.get(self.captcha_type, 80)
                r.rpush(self.pool_key, result["request"])
                r.expire(self.pool_key, ttl)
                return
            if result.get("request") != "CAPCHA_NOT_READY":
                return

    def get_token(self):
        return r.lpop(self.pool_key)


# Usage
pool = TokenPool("6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", "https://example.com")
pool.start()

# When you need a token:
token = pool.get_token()

Ukuran kumpulan harus disesuaikan dengan kecepatan penyelesaian tipe yang Anda pakai. Cloudflare Turnstile umumnya selesai dalam waktu kurang dari 10 detik, sedangkan reCAPTCHA v2 kurang dari 60 detik, jadi kumpulan untuk reCAPTCHA v2 perlu lebih besar untuk menutup jeda pengisian ulang yang lebih panjang.

Versi Node.js dengan ioredis

Pola yang sama untuk pipeline Node.js, memakai ioredis dan axios. Struktur kunci, margin TTL, dan kunci dedupnya identik, jadi worker Python dan Node.js bisa berbagi satu instance Redis.

const Redis = require("ioredis");
const axios = require("axios");

const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
const API_KEY = process.env.CAPTCHAAI_API_KEY;

const TOKEN_TTLS = { recaptcha_v2: 80, recaptcha_v3: 80, hcaptcha: 80, turnstile: 250 };

function cacheKey(sitekey, pageurl) {
  return `captcha:token:${sitekey}:${pageurl}`;
}

async function getCachedToken(sitekey, pageurl) {
  const key = cacheKey(sitekey, pageurl);
  const token = await redis.lpop(key);
  if (token) {
    const ttl = await redis.ttl(key);
    if (ttl > 10) return token;
  }
  return null;
}

async function solveWithCache(sitekey, pageurl, type = "recaptcha_v2") {
  // Check cache
  const cached = await getCachedToken(sitekey, pageurl);
  if (cached) return { solution: cached, source: "cache" };

  // Dedup lock
  const lockKey = `captcha:lock:${sitekey}:${pageurl}`;
  const locked = await redis.set(lockKey, "1", "NX", "EX", 120);
  if (!locked) {
    for (let i = 0; i < 60; i++) {
      await new Promise((r) => setTimeout(r, 2000));
      const waitCached = await getCachedToken(sitekey, pageurl);
      if (waitCached) return { solution: waitCached, source: "cache_wait" };
    }
    return { error: "TIMEOUT_WAITING" };
  }

  try {
    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) 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 key = cacheKey(sitekey, pageurl);
        const ttl = TOKEN_TTLS[type] || 80;
        await redis.rpush(key, poll.data.request);
        await redis.expire(key, ttl);
        return { solution: poll.data.request, source: "api" };
      }
      if (poll.data.request !== "CAPCHA_NOT_READY") return { error: poll.data.request };
    }
    return { error: "TIMEOUT" };
  } finally {
    await redis.del(lockKey);
  }
}

Memantau hit rate cache

Cache tanpa metrik hanyalah tebakan. Statistik keyspace Redis cukup untuk menjawab pertanyaan pentingnya: apakah cache ini benar-benar mengurangi jumlah solve?

def cache_stats():
    info = r.info("stats")
    hits = info.get("keyspace_hits", 0)
    misses = info.get("keyspace_misses", 0)
    total = hits + misses
    return {
        "hit_rate": f"{hits / total * 100:.1f}%" if total else "0%",
        "hits": hits,
        "misses": misses,
        "active_keys": r.dbsize()
    }

Kalau hit rate Anda bertahan di bawah 10% selama beberapa hari, cache itu tidak menghasilkan apa pun selain satu titik kegagalan tambahan — pola trafik Anda kemungkinan terlalu tersebar. Di atas 30%, penghematan thread mulai terasa nyata pada jam sibuk.

Masalah yang sering muncul di produksi

Masalah Penyebab Solusi
Token dari cache ditolak situs target Token sudah mati sebelum terkirim Perbesar margin TTL, atau kirim token segera setelah diambil
Target terkunci padahal tidak ada solve berjalan Worker mati sebelum melepas kunci Andalkan TTL kunci (120 detik) dan lepas kunci di blok finally
Kumpulan token selalu kosong Waktu penyelesaian lebih lambat daripada laju konsumsi Perbesar pool_size atau tambah thread pengisi ulang
Memori Redis terus bertambah Ada kunci tanpa TTL Pastikan setiap kunci punya TTL; audit dengan redis-cli --bigkeys

Pertanyaan umum

Berapa banyak thread CaptchaAI yang saya butuhkan jika sudah memakai cache Redis?

Hitung dari solve unik per menit setelah dedup, bukan dari jumlah worker. Kunci dedup menyatukan permintaan bersamaan untuk target yang sama menjadi satu solve saja. Karena setiap paket memberi solve tanpa batas per thread, yang perlu Anda ukur hanyalah puncak solve bersamaan.

Apakah pola ini berlaku untuk Cloudflare Turnstile dan GeeTest v3?

Ya. Struktur kuncinya sama persis; yang berubah hanya nilai TTL karena umur token tiap tipe berbeda. Pola ini berlaku untuk tipe yang didukung CaptchaAI: reCAPTCHA v2 dan v3, Cloudflare Turnstile dan Cloudflare Challenge, GeeTest v3, serta CAPTCHA gambar dan teks. GeeTest v4 masih berstatus segera hadir.

Apa yang terjadi jika Redis mati saat runtime?

Perlakukan cache sebagai lapisan opsional: bungkus setiap pembacaannya dengan penanganan kesalahan dan lanjutkan langsung ke in.php bila Redis tidak merespons. Redis di sini mempercepat, bukan menjadi syarat.

Kenapa hit rate saya rendah padahal TTL sudah panjang?

Biasanya karena kunci terlalu spesifik. Jika pageurl menyertakan query string yang berubah tiap permintaan, setiap solve membuat kunci baru yang tidak pernah dipakai ulang. Normalkan URL sebelum membentuk kunci cache.

Langkah selanjutnya

Uji pola ini pada satu target, ukur hit rate selama sehari, baru perluas ke seluruh worker — ambil API key CaptchaAI Anda untuk memulai.

Panduan terkait:

Komentar dinonaktifkan untuk artikel ini.