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_cookieCloudflare — 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:
- 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.
- Cache tidak pernah hit — parameter permintaan berbeda antar panggilan sehingga hash key-nya tidak pernah sama. Perbaikan: normalisasi parameter sebelum di-hash.
- Token basi tersimpan di Redis — TTL yang di-set terlalu panjang dibanding masa berlaku token aslinya. Perbaikan: turunkan TTL dan sisakan safety margin.
- 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.