Jawaban singkat: bentuk dedup key dari hash SHA-256 kombinasi
method+sitekey+pageurl, simpan sebagai key Redis ber-TTL atau lock ID PostgreSQL — begitu worker kedua menemukan key yang sama, ia memakai hasil yang sudah ada tanpa memanggil API CaptchaAI lagi.
Kalau lebih dari satu worker scraping Anda menargetkan sitekey dan halaman yang sama, kemungkinan besar Anda sedang membayar untuk CAPTCHA yang sudah diselesaikan detik itu juga oleh worker lain. Retry agresif, refresh halaman saat timeout, dan pesan queue yang di-replay semuanya berujung pada request solve kembar — dan setiap kembaran tetap menghabiskan kredit API secara penuh. Jawabannya bukan mengurangi jumlah worker, melainkan menambahkan satu lapisan dedup yang mengenali request kembar dalam hitungan milidetik.
Pola ini relevan buat tim scraping freelance atau agensi price-monitoring di Indonesia yang menjalankan beberapa worker paralel di region seperti AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta) — makin banyak worker yang jalan bersamaan memantau produk yang sama, makin besar peluang dua worker menabrak CAPTCHA identik nyaris bersamaan.
Kenapa Worker Paralel Membuat Anda Bayar CAPTCHA Berkali-Kali
Empat pola berikut adalah penyebab paling umum di balik request solve CAPTCHA yang terduplikasi pada pipeline automation:
| Skenario | Penyebab | Dampak Biaya |
|---|---|---|
| Retry sebelum hasil selesai | Retry logic terlalu agresif | Biaya 2–5x per CAPTCHA |
| Banyak worker menyasar target sama | Tidak ada koordinasi antar-worker | Solve paralel yang mubazir |
| Refresh halaman memicu ulang | Retry di sisi frontend saat timeout | Solve tambahan per refresh |
| Pesan queue di-replay | Jaminan pengiriman at-least-once | Solve duplikat per replay |
Dampaknya bertambah cepat begitu skala worker naik:
- Satu pola saja yang dibiarkan sudah bisa menggandakan biaya solve untuk CAPTCHA yang sama persis.
- Pada pipeline dengan puluhan worker paralel, akumulasi keempat pola ini bisa jadi porsi signifikan dari total pemakaian thread bulanan Anda.
Menyusun Dedup Key yang Andal
Dedup key dibentuk dari kombinasi parameter request yang membuat dua CAPTCHA benar-benar identik, bukan sekadar mirip. Untuk sebagian besar tipe CAPTCHA, kombinasi method, sitekey, dan pageurl sudah cukup unik. Hash SHA-256 memampatkan kombinasi ini jadi key pendek yang aman disimpan sebagai key Redis atau lock ID PostgreSQL:
import hashlib
def dedup_key(method, sitekey, pageurl):
"""Generate a deduplication key for a CAPTCHA solve request."""
raw = f"{method}:{sitekey}:{pageurl}"
return f"captcha:dedup:{hashlib.sha256(raw.encode()).hexdigest()[:16]}"
16 karakter pertama dari hash biasanya sudah cukup untuk menghindari collision pada volume worker yang wajar; kalau volume Anda sangat tinggi, naikkan panjangnya. Komponen key berbeda sedikit tergantung tipe CAPTCHA:
| Tipe CAPTCHA | Komponen Key |
|---|---|
| reCAPTCHA v2 | method + sitekey + pageurl |
| reCAPTCHA v3 | method + sitekey + pageurl + action |
| hCaptcha | method + sitekey + pageurl |
| Turnstile | method + sitekey + pageurl |
| Image CAPTCHA | method + hash body (isi gambar) |
Untuk reCAPTCHA v3, jangan lewatkan action — action berbeda tetap dihitung sebagai request berbeda meski sitekey dan pageurl-nya identik.
Redis untuk Deduplikasi Real-Time
Redis paling praktis untuk dedup karena punya operasi atomik SET NX dan TTL bawaan. Worker pertama yang mengunci key langsung submit ke CaptchaAI; worker berikutnya yang datang dalam window yang sama cukup menunggu hasil dari worker pertama, tanpa mengirim request baru.
Implementasi Python
import os
import time
import json
import hashlib
import redis
import requests
r = redis.Redis(
host=os.environ.get("REDIS_HOST", "localhost"),
port=int(os.environ.get("REDIS_PORT", 6379)),
decode_responses=True
)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Dedup window: how long to consider a request "in progress"
DEDUP_TTL = 180 # seconds
def dedup_key(method, sitekey, pageurl, extra=""):
raw = f"{method}:{sitekey}:{pageurl}:{extra}"
return f"captcha:dedup:{hashlib.sha256(raw.encode()).hexdigest()[:16]}"
def solve_with_dedup(sitekey, pageurl, method="userrecaptcha"):
key = dedup_key(method, sitekey, pageurl)
# Check if this request is already being solved
existing = r.get(key)
if existing:
state = json.loads(existing)
if state["status"] == "solving":
# Wait for the result
return wait_for_result(key)
elif state["status"] == "solved":
return {"solution": state["solution"], "source": "dedup_cache"}
elif state["status"] == "error":
pass # Allow retry on error
# Mark as solving
r.set(key, json.dumps({"status": "solving", "started": time.time()}), ex=DEDUP_TTL)
# Submit to CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": method,
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
r.set(key, json.dumps({"status": "error", "error": data.get("request")}), ex=30)
return {"error": data.get("request")}
captcha_id = data["request"]
# Poll for result
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:
solution = result["request"]
# Cache the result for other workers (short TTL since tokens expire)
r.set(key, json.dumps({
"status": "solved",
"solution": solution,
"solved_at": time.time()
}), ex=60) # Cache result for 60 seconds
return {"solution": solution, "source": "api"}
if result.get("request") != "CAPCHA_NOT_READY":
r.set(key, json.dumps({
"status": "error", "error": result.get("request")
}), ex=30)
return {"error": result.get("request")}
r.set(key, json.dumps({"status": "error", "error": "TIMEOUT"}), ex=30)
return {"error": "TIMEOUT"}
def wait_for_result(key, timeout=120):
"""Wait for another worker to finish solving."""
start = time.time()
while time.time() - start < timeout:
data = r.get(key)
if data:
state = json.loads(data)
if state["status"] == "solved":
return {"solution": state["solution"], "source": "dedup_wait"}
if state["status"] == "error":
return {"error": state.get("error", "UNKNOWN")}
time.sleep(2)
return {"error": "DEDUP_WAIT_TIMEOUT"}
Implementasi Node.js
Logikanya identik di Node.js — cek status di Redis, kunci key kalau belum ada worker lain yang sedang memprosesnya, submit ke in.php, lalu polling res.php sampai token siap:
const Redis = require("ioredis");
const axios = require("axios");
const crypto = require("crypto");
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const DEDUP_TTL = 180;
function dedupKey(method, sitekey, pageurl) {
const raw = `${method}:${sitekey}:${pageurl}`;
const hash = crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
return `captcha:dedup:${hash}`;
}
async function solveWithDedup(sitekey, pageurl, method = "userrecaptcha") {
const key = dedupKey(method, sitekey, pageurl);
// Check existing
const existing = await redis.get(key);
if (existing) {
const state = JSON.parse(existing);
if (state.status === "solving") return await waitForResult(key);
if (state.status === "solved") return { solution: state.solution, source: "dedup_cache" };
}
// Mark as solving
await redis.set(key, JSON.stringify({ status: "solving", started: Date.now() }), "EX", DEDUP_TTL);
// Submit
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method, googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
await redis.set(key, JSON.stringify({ status: "error", error: submit.data.request }), "EX", 30);
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) {
await redis.set(key, JSON.stringify({ status: "solved", solution: poll.data.request }), "EX", 60);
return { solution: poll.data.request, source: "api" };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
await redis.set(key, JSON.stringify({ status: "error", error: poll.data.request }), "EX", 30);
return { error: poll.data.request };
}
}
await redis.set(key, JSON.stringify({ status: "error", error: "TIMEOUT" }), "EX", 30);
return { error: "TIMEOUT" };
}
async function waitForResult(key, timeout = 120000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const data = await redis.get(key);
if (data) {
const state = JSON.parse(data);
if (state.status === "solved") return { solution: state.solution, source: "dedup_wait" };
if (state.status === "error") return { error: state.error };
}
await new Promise((r) => setTimeout(r, 2000));
}
return { error: "DEDUP_WAIT_TIMEOUT" };
}
Alternatif Tanpa Redis: PostgreSQL Advisory Lock
Kalau infrastruktur Anda sudah punya PostgreSQL tapi belum punya Redis, advisory lock bisa jadi lapisan dedup tanpa komponen tambahan. pg_try_advisory_lock mengunci secara non-blocking; worker yang gagal mendapat lock otomatis menunggu lewat pg_advisory_lock, lalu membaca hasil dari tabel cache:
import psycopg2
def solve_with_pg_dedup(conn, sitekey, pageurl):
"""Use PostgreSQL advisory locks for deduplication."""
# Generate a numeric lock key from the dedup key
lock_id = hash(f"{sitekey}:{pageurl}") & 0x7FFFFFFF
cursor = conn.cursor()
# Try to acquire advisory lock (non-blocking)
cursor.execute("SELECT pg_try_advisory_lock(%s)", (lock_id,))
acquired = cursor.fetchone()[0]
if not acquired:
# Another worker is solving — wait for result
cursor.execute("SELECT pg_advisory_lock(%s)", (lock_id,))
# Lock acquired means other worker finished — check cache
cursor.execute(
"SELECT solution FROM captcha_cache "
"WHERE sitekey = %s AND pageurl = %s "
"AND created_at > NOW() - INTERVAL '60 seconds'",
(sitekey, pageurl)
)
row = cursor.fetchone()
cursor.execute("SELECT pg_advisory_unlock(%s)", (lock_id,))
if row:
return {"solution": row[0], "source": "pg_cache"}
return {"error": "NO_CACHED_RESULT"}
try:
# Solve the CAPTCHA
solution = solve_via_api(sitekey, pageurl)
if solution:
cursor.execute(
"INSERT INTO captcha_cache (sitekey, pageurl, solution) "
"VALUES (%s, %s, %s)",
(sitekey, pageurl, solution)
)
conn.commit()
return {"solution": solution} if solution else {"error": "SOLVE_FAILED"}
finally:
cursor.execute("SELECT pg_advisory_unlock(%s)", (lock_id,))
Pendekatan ini sedikit lebih lambat dibanding Redis pada throughput sangat tinggi, tapi solid untuk volume menengah dan menghindari dependency tambahan di stack Anda.
Mengukur Penghematan dari Dedup
Angka
savings_pctdi bawah ini murni hasil pengukuran internal pipeline Anda, bukan klaim resmi CaptchaAI — hasilnya bergantung pola traffic dan jumlah worker paralel yang Anda jalankan.
Catat setiap sumber hasil — dedup_cache, dedup_wait, atau api — supaya Anda tahu persis berapa persen request yang berhasil dihindari:
def track_dedup_stats(source):
"""Increment counters for dedup tracking."""
today = time.strftime("%Y-%m-%d")
r.hincrby(f"dedup:stats:{today}", source, 1)
r.expire(f"dedup:stats:{today}", 7 * 86400)
def get_dedup_report():
today = time.strftime("%Y-%m-%d")
stats = r.hgetall(f"dedup:stats:{today}")
total = sum(int(v) for v in stats.values())
saved = int(stats.get("dedup_cache", 0)) + int(stats.get("dedup_wait", 0))
return {
"total_requests": total,
"deduplicated": saved,
"savings_pct": f"{saved / total * 100:.1f}%" if total else "0%",
"breakdown": stats
}
Laporan harian sesederhana ini biasanya cukup untuk meyakinkan tim bahwa dedup layer sepadan dengan biaya maintenance-nya.
Masalah Umum dan Cara Mengatasinya
Hal yang paling sering meleset saat tim pertama kali menerapkan dedup layer ini:
| Masalah | Penyebab | Solusi |
|---|---|---|
| Dedup key bentrok (collision) | Hash terlalu pendek atau parameter kurang lengkap | Sertakan semua parameter spesifik tipe CAPTCHA dalam key; naikkan panjang hash |
| Worker menunggu sampai timeout | Worker yang sedang memprosesnya crash sebelum selesai | TTL pada status solving otomatis kedaluwarsa (180 detik) |
| Hasil cache sudah basi | Token sudah kedaluwarsa tapi cache masih dianggap valid | Set TTL cache hasil lebih pendek dari umur token (60 detik untuk reCAPTCHA) |
| Race condition saat submit | Dua worker memeriksa key secara bersamaan | Gunakan SET NX (set-if-not-exists) supaya akuisisi lock atomik |
Kalau worker Anda berjalan di banyak region sekaligus, pastikan semuanya terhubung ke instance Redis atau PostgreSQL yang sama — dedup key yang tersebar di instance yang tidak saling tahu tidak akan pernah saling mendeteksi duplikat.
Yang Sering Ditanyakan Tentang Dedup Request CAPTCHA
Apakah dedup bikin proses solve jadi lebih lambat?
Tidak untuk request yang memang duplikat — begitu dedup key ditemukan di cache, hasil dikembalikan dalam hitungan milidetik tanpa menyentuh API sama sekali. Request pertama yang benar-benar baru tetap melalui waktu solve normal karena memang belum ada hasil yang bisa dipakai ulang.
Idealnya, berapa lama TTL cache hasil solve?
Selalu lebih pendek dari umur token itu sendiri. Pada contoh di atas, TTL 60 detik aman untuk reCAPTCHA karena tokennya biasanya kedaluwarsa dalam rentang waktu serupa; sesuaikan turun kalau tipe CAPTCHA Anda punya token dengan umur lebih singkat.
Bagaimana mencegah race condition saat dua worker submit hampir bersamaan?
Jangan pakai pola cek-lalu-set biasa — pakai operasi atomik:
- Redis:
SET NX(set-if-not-exists) dalam satu perintah. - PostgreSQL:
pg_try_advisory_lockuntuk akuisisi lock non-blocking.
Operasi atomik memastikan hanya satu worker yang benar-benar menang mengunci key; worker lainnya otomatis masuk jalur menunggu hasil.
Haruskah proxy yang berbeda dipisahkan dalam dedup key?
Tidak perlu. Token hasil solve tetap berlaku terlepas dari proxy mana yang dipakai untuk menyelesaikannya, jadi menyertakan proxy di dedup key justru menggagalkan tujuan deduplikasi itu sendiri.
Apakah image CAPTCHA (OCR) juga perlu dedup?
Perlu, dengan sedikit modifikasi: gunakan hash dari konten gambar sebagai bagian dedup key, bukan sitekey atau pageurl. Gambar yang identik selalu menghasilkan teks yang sama, jadi dedup tetap efektif untuk tipe ini.
Mulai Terapkan Dedup di Pipeline Anda
Setiap request solve duplikat yang lolos adalah kredit API yang terbuang percuma — dan pada plan thread-based seperti BASIC ($15/bulan, 5 thread) atau STANDARD ($30/bulan, 15 thread), thread yang harusnya bisa melayani request baru malah habis untuk menyelesaikan CAPTCHA yang jawabannya sebenarnya sudah ada. Dapatkan API key CaptchaAI Anda dan terapkan dedup layer ini hari ini. Panduan terkait:
- Kelola TTL token CAPTCHA dengan Redis
- Menjaga session state di banyak worker terdistribusi
- Strategi pemulihan error CAPTCHA secara batch