Tutorials

Menyimpan Token CAPTCHA untuk Digunakan Kembali

Tidak semua panggilan API CAPTCHA perlu dibayar dua kali. Token Cloudflare Turnstile bertahan hingga ~300 detik, dan cookie Cloudflare Challenge bisa dipakai ulang selama 15–30 menit — artinya satu kali penyelesaian bisa menutup puluhan permintaan berikutnya tanpa panggilan API baru. Masalahnya, tidak semua tipe CAPTCHA berperilaku sama: reCAPTCHA v2 dan v3 nyaris selalu sekali pakai. Panduan ini memetakan tipe token mana yang aman di-cache, berapa lama TTL idealnya, dan cara menerapkannya di Python, Node.js, dan Redis tanpa mengirim token basi ke situs target.


Kapan Caching Token Benar-Benar Menghemat Biaya

Caching hanya bermanfaat pada pola trafik tertentu:

  • Form yang sama dikirim berkali-kali — misalnya retry pada alur pengujian atau checkout yang identik.
  • Cookie qa_validation_cookie Cloudflare — satu penyelesaian membuka seluruh sesi, bukan hanya satu permintaan.
  • OCR massal — gambar CAPTCHA statis yang sama muncul berulang pada dataset yang sudah dikenal.
  • Validasi awal — token disiapkan sebelum benar-benar dibutuhkan, supaya alur utama tidak menunggu penyelesaian CAPTCHA.

Di luar keempat pola ini, caching biasanya sia-sia: situs yang memvalidasi tiap token hanya sekali, token yang terikat pada tindakan atau sesi tertentu, atau token yang sudah kedaluwarsa. Memaksakan reuse di sini hanya menambah kegagalan permintaan, bukan menghemat biaya.

Bagi tim price-monitoring atau scraping freelance di Indonesia — pola kerja yang umum di platform seperti Upwork atau Fastwork — biaya panggilan API menumpuk cepat begitu volume mencapai ribuan permintaan per hari dari worker yang dideploy di region seperti AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta). Meng-cache token Cloudflare Challenge saja pada domain yang sama bisa memangkas panggilan API secara signifikan — berpotensi menekan biaya lebih dari 90% untuk scraping frekuensi tinggi ke domain yang sama, karena satu sesi mencakup puluhan permintaan berikutnya.


Masa Berlaku Token per Tipe CAPTCHA

Tipe CAPTCHA Masa berlaku token Bisa di-cache? Catatan
reCAPTCHA v2 ~120 detik Terbatas Umumnya sekali pakai per situs
reCAPTCHA v3 ~120 detik Terbatas Skor bisa bervariasi per permintaan
reCAPTCHA Enterprise ~120 detik Tidak Spesifik tindakan, sekali pakai
Cloudflare Turnstile ~300 detik Ya, dalam jendela waktu Token bisa dipakai ulang hingga kedaluwarsa
Cloudflare Challenge qa_validation_cookie ~15–30 menit Ya Cookie bisa dipakai ulang untuk satu sesi
Image OCR N/A (hasil teks) Ya Hasil tidak pernah kedaluwarsa
GeeTest v3 ~60 detik Tidak Spesifik per tantangan

Intinya: cookie Cloudflare Challenge (qa_validation_cookie) dan hasil Image OCR paling mudah di-cache karena tidak terikat satu aksi tunggal. Token reCAPTCHA punya jendela pendek dan pada kebanyakan situs hanya berlaku sekali — jangan andalkan reuse untuk tipe ini tanpa menguji dulu.


Implementasi Cache Token: Python, Node.js, dan Redis

Tiga versi berikut menerapkan pola yang sama — cek cache, solve kalau tidak ada, simpan dengan TTL — di tiga stack berbeda yang umum dipakai tim automation.

Python — cache di memori proses

Cocok untuk script tunggal atau worker yang tidak dibagi antar proses:

import time
import hashlib
from typing import Optional
import requests

SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


class TokenCache:
    def __init__(self):
        self.cache = {}

    def _key(self, method: str, params: dict) -> str:
        # Cache key from method + stable params
        stable = {k: v for k, v in sorted(params.items())
                  if k not in ("key", "json")}
        raw = f"{method}:{stable}"
        return hashlib.sha256(raw.encode()).hexdigest()[:16]

    def get(self, method: str, params: dict) -> Optional[str]:
        key = self._key(method, params)
        entry = self.cache.get(key)
        if entry and entry["expires_at"] > time.time():
            print(f"Cache HIT: {key}")
            return entry["token"]
        if entry:
            del self.cache[key]
        return None

    def set(self, method: str, params: dict, token: str, ttl: int):
        key = self._key(method, params)
        self.cache[key] = {
            "token": token,
            "expires_at": time.time() + ttl,
        }
        print(f"Cached: {key} (TTL: {ttl}s)")

    def invalidate(self, method: str, params: dict):
        key = self._key(method, params)
        self.cache.pop(key, None)

    def cleanup(self):
        now = time.time()
        expired = [k for k, v in self.cache.items() if v["expires_at"] <= now]
        for k in expired:
            del self.cache[k]


# TTL per CAPTCHA type
TTL_MAP = {
    "userrecaptcha": 100,       # 120s lifetime, 20s safety margin
    "turnstile": 240,           # 300s lifetime, 60s margin
    "cloudflare_challenge": 900,# 15min lifetime, 5min margin
    "base64": 86400,            # OCR result never expires — cache 24h
}


class CachedSolver:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.cache = TokenCache()

    def solve(self, method: str, params: dict) -> str:
        # Check cache first
        cached = self.cache.get(method, params)
        if cached:
            return cached

        # Solve via API
        token = self._api_solve(method, params)
        ttl = TTL_MAP.get(method, 60)
        self.cache.set(method, params, token, ttl)
        return token

    def _api_solve(self, method: str, params: dict) -> str:
        data = {
            "key": self.api_key,
            "method": method,
            "json": 1,
            **params
        }

        resp = requests.post(SUBMIT_URL, data=data, timeout=15)
        result = resp.json()

        if result.get("status") != 1:
            raise Exception(result.get("error_text", result.get("request")))

        task_id = result["request"]
        return self._poll(task_id)

    def _poll(self, task_id: str, max_wait: int = 120) -> str:
        elapsed = 0
        while elapsed < max_wait:
            time.sleep(5)
            elapsed += 5

            resp = requests.get(RESULT_URL, params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            }, timeout=10)
            result = resp.json()

            if result.get("status") == 1:
                return result["request"]
            if result.get("request") == "CAPCHA_NOT_READY":
                continue

            raise Exception(result.get("error_text", result.get("request")))

        raise Exception(f"Timeout: {task_id}")


# Usage
solver = CachedSolver(api_key="YOUR_API_KEY")

# First call — hits API
token1 = solver.solve("turnstile", {
    "sitekey": "0x4AAAA-SITEKEY",
    "pageurl": "https://example.com"
})
print(f"Token 1: {token1[:40]}...")

# Second call within TTL — cache hit, no API call
token2 = solver.solve("turnstile", {
    "sitekey": "0x4AAAA-SITEKEY",
    "pageurl": "https://example.com"
})
print(f"Token 2: {token2[:40]}...")
print(f"Same token: {token1 == token2}")  # True

Node.js — pola yang identik

Memakai Map bawaan sebagai penyimpanan cache:

const axios = require("axios");
const crypto = require("crypto");

const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

const TTL_MAP = {
  userrecaptcha: 100,
  turnstile: 240,
  cloudflare_challenge: 900,
  base64: 86400,
};

class TokenCache {
  constructor() {
    this.cache = new Map();
  }

  _key(method, params) {
    const stable = Object.entries(params)
      .filter(([k]) => k !== "key" && k !== "json")
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([k, v]) => `${k}=${v}`)
      .join("&");
    return crypto.createHash("sha256").update(`${method}:${stable}`).digest("hex").slice(0, 16);
  }

  get(method, params) {
    const key = this._key(method, params);
    const entry = this.cache.get(key);
    if (entry && entry.expiresAt > Date.now()) {
      console.log(`Cache HIT: ${key}`);
      return entry.token;
    }
    if (entry) this.cache.delete(key);
    return null;
  }

  set(method, params, token, ttlMs) {
    const key = this._key(method, params);
    this.cache.set(key, { token, expiresAt: Date.now() + ttlMs });
    console.log(`Cached: ${key} (TTL: ${ttlMs / 1000}s)`);
  }
}

class CachedSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.cache = new TokenCache();
  }

  async solve(method, params) {
    const cached = this.cache.get(method, params);
    if (cached) return cached;

    const token = await this._apiSolve(method, params);
    const ttl = (TTL_MAP[method] || 60) * 1000;
    this.cache.set(method, params, token, ttl);
    return token;
  }

  async _apiSolve(method, params) {
    const resp = await axios.post(SUBMIT_URL, null, {
      params: { key: this.apiKey, method, json: 1, ...params },
      timeout: 15000,
    });

    if (resp.data.status !== 1) {
      throw new Error(resp.data.error_text || resp.data.request);
    }

    return this._poll(resp.data.request);
  }

  async _poll(taskId, maxWait = 120000) {
    let elapsed = 0;
    while (elapsed < maxWait) {
      await new Promise((r) => setTimeout(r, 5000));
      elapsed += 5000;

      const resp = await axios.get(RESULT_URL, {
        params: { key: this.apiKey, action: "get", id: taskId, json: 1 },
        timeout: 10000,
      });

      if (resp.data.status === 1) return resp.data.request;
      if (resp.data.request === "CAPCHA_NOT_READY") continue;

      throw new Error(resp.data.error_text || resp.data.request);
    }
    throw new Error("Timeout");
  }
}

// Usage
(async () => {
  const solver = new CachedSolver("YOUR_API_KEY");

  const token1 = await solver.solve("turnstile", {
    sitekey: "0x4AAAA-SITEKEY",
    pageurl: "https://example.com",
  });
  console.log(`Token 1: ${token1.slice(0, 40)}...`);

  const token2 = await solver.solve("turnstile", {
    sitekey: "0x4AAAA-SITEKEY",
    pageurl: "https://example.com",
  });
  console.log(`Token 2: ${token2.slice(0, 40)}...`);
  console.log(`Same token: ${token1 === token2}`);
})();

Redis — untuk worker terdistribusi

Kalau worker Anda berjalan di beberapa proses atau beberapa mesin sekaligus, cache dalam memori tidak akan saling terlihat satu sama lain. Redis menyelesaikan ini — satu cache yang dibagi oleh semua worker, di region mana pun mereka dideploy:

import redis
import json

r = redis.Redis(host="localhost", port=6379, db=0)

def cache_token(method, params, token, ttl):
    key = f"captcha:{method}:{hash(frozenset(params.items()))}"
    r.setex(key, ttl, token)

def get_cached_token(method, params):
    key = f"captcha:{method}:{hash(frozenset(params.items()))}"
    return r.get(key)

Redis otomatis menangani kedaluwarsa TTL dan tetap konsisten walau jumlah proses bertambah atau berkurang.


Validasi Awal: Siapkan Token Sebelum Dibutuhkan

Alih-alih menunggu CAPTCHA diselesaikan saat request masuk, validasi awal menyiapkan buffer token yang siap pakai di background, mengorbankan sedikit token yang mungkin terbuang demi menghilangkan waktu tunggu:

from collections import deque
from threading import Thread

token_buffer = deque(maxlen=5)

def pre_solve_worker(solver, method, params):
    while True:
        if len(token_buffer) < 3:
            try:
                token = solver._api_solve(method, params)
                ttl = TTL_MAP.get(method, 60)
                token_buffer.append({
                    "token": token,
                    "expires_at": time.time() + ttl
                })
            except Exception as e:
                print(f"validasi awal di staging failed: {e}")
        time.sleep(2)

# Start validasi staging in background
thread = Thread(
    target=pre_solve_worker,
    args=(solver, "turnstile", {"sitekey": "0x4AAAA-KEY", "pageurl": "https://example.com"}),
    daemon=True
)
thread.start()

# Consume token QA staging
def get_presolved():
    while token_buffer:
        entry = token_buffer.popleft()
        if entry["expires_at"] > time.time():
            return entry["token"]
    return None

Kapan Harus Menghapus Token dari Cache

Empat pemicu berikut wajib langsung menghapus (invalidasi) entry di cache, bukan menunggu TTL habis dengan sendirinya:

  • Token ditolak oleh situs target → invalidasi entry itu dan solve ulang, jangan dipakai lagi.
  • TTL sudah kedaluwarsa → otomatis dihapus dari cache oleh mekanisme TTL/expires_at.
  • Proxy berubah → invalidasi semua token Cloudflare terkait, karena token itu terikat IP.
  • Konfigurasi CAPTCHA di situs target diperbarui → hapus semua token yang di-cache untuk situs tersebut, bukan hanya satu entry.

Cache Sering Meleset? Penyebab dan Solusinya

Urutkan diagnosis berikut dari gejala yang paling sering muncul di produksi:

  1. Token yang di-cache ditolak — penyebabnya hampir selalu token sudah kedaluwarsa atau memang sekali pakai di situs itu. Perbaikan: kurangi TTL atau nonaktifkan cache untuk tipe CAPTCHA tersebut.
  2. Cache tidak pernah hit — parameter permintaan berbeda antar panggilan sehingga hash key-nya tidak pernah sama. Perbaikan: normalisasi parameter sebelum di-hash.
  3. Token basi tersimpan di Redis — TTL yang di-set terlalu panjang dibanding masa berlaku token aslinya. Perbaikan: turunkan TTL dan sisakan safety margin.
  4. Penggunaan memory terus naik — tidak ada proses pembersihan entry kedaluwarsa. Perbaikan: panggil cleanup() secara berkala, atau pindah ke Redis yang punya TTL bawaan.

Pertanyaan Umum Seputar Caching Token CAPTCHA

Bisakah token reCAPTCHA v2 disimpan dan dipakai ulang?

Terkadang, tapi jangan jadikan asumsi default. Banyak situs hanya menerima token satu kali. Cara mengujinya: kirim token yang sama dua kali — jika pengiriman kedua tetap berhasil, cache berfungsi untuk situs tersebut; jika ditolak, matikan caching untuk userrecaptcha di domain itu.

Berapa TTL yang aman untuk cache token Cloudflare Turnstile?

Turnstile bertahan sekitar 300 detik, jadi TTL cache sebaiknya diset lebih pendek — sekitar 240 detik — untuk menyisakan margin keamanan 60 detik. Margin ini mencegah token dikirim tepat saat kedaluwarsa akibat latensi jaringan.

Kenapa token yang sudah di-cache tetap ditolak situs target?

Penyebab paling umum: token sudah sekali pakai meski TTL cache belum habis, atau token terikat pada sesi/IP yang sudah berubah — misalnya karena rotasi proxy. Cek daftar pemicu invalidasi di atas sebelum menaikkan TTL lebih jauh.

Apakah validasi awal sepadan untuk semua use case?

Sepadan jika volume permintaan Anda cukup dapat diprediksi. Validasi awal menghilangkan waktu tunggu di jalur utama, tapi mengorbankan token yang mungkin terbuang kalau permintaan tiba-tiba turun — jangan pakai untuk trafik yang sangat sporadis.

Apakah menyimpan token CAPTCHA berisiko dari sisi hukum?

Token itu sendiri hanya bukti bahwa sebuah tantangan sudah diselesaikan, jadi bukan data pribadi. Namun jika alur kerja Anda sekaligus mengambil data dari situs pihak ketiga, tetap perhatikan UU Pelindungan Data Pribadi (UU 27/2022) — proses hanya data yang memang berwenang Anda akses. Ini bukan nasihat hukum.


Mulai Cache Token CAPTCHA dengan CaptchaAI

Terapkan pola di atas dengan endpoint CaptchaAI dan mulai hemat biaya panggilan CAPTCHA di captchaai.com.

Baca Juga

Komentar dinonaktifkan untuk artikel ini.