Kalau worker Anda menjalankan CaptchaAI dalam jumlah besar, pertanyaan yang paling sering muncul bukan lagi "apakah CAPTCHA-nya terselesaikan", tapi "jenis mana yang paling sering error, jam berapa, dan berapa lama rata-rata prosesnya". MongoDB pas untuk menjawab pertanyaan itu: skema dokumennya bebas dari migrasi tiap kali menambah jenis CAPTCHA baru, dan aggregation pipeline bawaannya cukup untuk menghitung tingkat keberhasilan, waktu penyelesaian, serta pola error tanpa perlu data warehouse terpisah.
Panduan ini membangun pipeline pencatatan solve dari nol: skema dokumen, koneksi dan index, fungsi kirim-dan-simpan, sampai kueri analitik siap pakai — dalam Python dan Node.js.
Kenapa Skema Fleksibel MongoDB Cocok untuk Data CAPTCHA
Setiap jenis CAPTCHA punya kolom yang berbeda: reCAPTCHA butuh googlekey, Cloudflare Turnstile butuh sitekey, CAPTCHA gambar/OCR butuh body berisi base64. Kalau Anda memakai database relasional, setiap kali menambah jenis CAPTCHA baru berarti migrasi skema — kolom baru, index baru, downtime kecil. Dokumen tanpa skema MongoDB menghindari masalah ini sepenuhnya: satu koleksi solves menampung reCAPTCHA, Turnstile, GeeTest v3, sampai BLS tanpa perlu ALTER TABLE sama sekali.
Ini juga cocok dengan cara tim automation Indonesia biasanya bekerja — banyak yang mengerjakan beberapa proyek klien sekaligus (freelance scraping lewat Upwork/Fastwork, agensi price-monitoring, tim data startup). Field metadata.project di skema di bawah ini sengaja dibuat generik supaya satu koleksi bisa melacak solve dari beberapa proyek berbeda tanpa perlu database terpisah per klien.
Skema Dokumen per Solve
Simpan setiap upaya solve sebagai satu dokumen, termasuk metadata operasional yang Anda butuhkan untuk debugging dan pelaporan nanti:
{
"_id": "ObjectId",
"captcha_id": "12345678",
"type": "recaptcha_v2",
"method": "userrecaptcha",
"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"pageurl": "https://example.com/form",
"status": "solved",
"solution": "03AGdBq26...",
"error": null,
"submitted_at": "2026-04-04T10:15:30.000Z",
"solved_at": "2026-04-04T10:15:45.000Z",
"elapsed_ms": 15000,
"polls": 3,
"proxy_used": true,
"cost": 0.00299,
"metadata": {
"project": "price-monitor",
"worker_id": "worker-3",
"target_domain": "example.com"
}
}
Membangun Pipeline-nya dengan Python
Ada empat bagian: koneksi ke MongoDB, index untuk performa, fungsi yang mengirim CAPTCHA ke CaptchaAI sambil mencatat hasilnya, lalu kueri untuk membaca datanya kembali.
Koneksi ke MongoDB
Muat URI koneksi dan API key dari environment variable — jangan hardcode di kode sumber:
import os
import time
from datetime import datetime, timezone
from pymongo import MongoClient, ASCENDING, DESCENDING
import requests
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
client = MongoClient(MONGO_URI)
db = client["captcha_tracking"]
solves = db["solves"]
Index yang Wajib Ada Sebelum Production
Tanpa index yang tepat, kueri aggregation di koleksi besar bisa melambat drastis begitu volume solve naik. Empat index di bawah menutupi pola akses yang paling umum: urutan waktu, filter per jenis dan status, filter per proyek, dan filter per domain target. Index terakhir memakai TTL supaya dokumen lama terhapus otomatis — atur expireAfterSeconds sesuai kebijakan retensi Anda (lihat bagian retensi data di bawah):
def setup_indexes():
solves.create_index([("submitted_at", DESCENDING)])
solves.create_index([("type", ASCENDING), ("status", ASCENDING)])
solves.create_index([("metadata.project", ASCENDING)])
solves.create_index([("metadata.target_domain", ASCENDING)])
solves.create_index(
[("submitted_at", ASCENDING)],
expireAfterSeconds=90 * 24 * 3600, # Auto-delete after 90 days
name="ttl_cleanup"
)
setup_indexes()
Kalau MongoDB Anda berjalan di region Asia Tenggara — AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta) — jalankan worker CaptchaAI di region yang sama untuk memangkas round-trip ke database. Selisihnya kecil per solve, tapi terasa saat volume ribuan solve per hari dan jaringan mobile-first jadi bottleneck di sisi worker.
Kirim CAPTCHA dan Simpan Hasilnya
Fungsi ini menyimpan dokumen submitted lebih dulu, mengirim task ke in.php, lalu polling res.php sampai statusnya berubah — persis pola kirim → simpan task ID → polling → pakai token yang berlaku untuk semua jenis CAPTCHA di CaptchaAI:
def solve_and_store(sitekey, pageurl, captcha_type="recaptcha_v2", metadata=None):
record = {
"type": captcha_type,
"method": "userrecaptcha",
"sitekey": sitekey,
"pageurl": pageurl,
"status": "submitted",
"submitted_at": datetime.now(timezone.utc),
"metadata": metadata or {}
}
result = solves.insert_one(record)
doc_id = result.inserted_id
# Submit to 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:
solves.update_one(
{"_id": doc_id},
{"$set": {"status": "error", "error": data.get("request")}}
)
return None
captcha_id = data["request"]
solves.update_one(
{"_id": doc_id},
{"$set": {"captcha_id": captcha_id, "status": "polling"}}
)
# Poll for result
polls = 0
for _ in range(60):
time.sleep(5)
polls += 1
poll_resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if poll_resp.get("status") == 1:
solved_at = datetime.now(timezone.utc)
elapsed_ms = int(
(solved_at - record["submitted_at"]).total_seconds() * 1000
)
solves.update_one({"_id": doc_id}, {"$set": {
"status": "solved",
"solution": poll_resp["request"],
"solved_at": solved_at,
"elapsed_ms": elapsed_ms,
"polls": polls
}})
return poll_resp["request"]
if poll_resp.get("request") != "CAPCHA_NOT_READY":
solves.update_one({"_id": doc_id}, {"$set": {
"status": "error",
"error": poll_resp.get("request"),
"polls": polls
}})
return None
solves.update_one({"_id": doc_id}, {"$set": {
"status": "timeout", "polls": polls
}})
return None
Kueri untuk Analitik
Empat fungsi ini menjawab pertanyaan operasional yang paling sering ditanyakan tim: berapa tingkat keberhasilan belakangan ini, jenis CAPTCHA mana yang paling lambat, bagaimana pola volume per jam, dan error apa yang paling sering muncul:
def get_success_rate(hours=24):
"""Success rate for the last N hours."""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
pipeline = [
{"$match": {"submitted_at": {"$gte": cutoff}}},
{"$group": {
"_id": "$status",
"count": {"$sum": 1}
}}
]
results = {r["_id"]: r["count"] for r in solves.aggregate(pipeline)}
total = sum(results.values())
solved = results.get("solved", 0)
return (solved / total * 100) if total else 0
def get_avg_solve_time_by_type():
"""Average solve time grouped by CAPTCHA type."""
pipeline = [
{"$match": {"status": "solved"}},
{"$group": {
"_id": "$type",
"avg_time_ms": {"$avg": "$elapsed_ms"},
"min_time_ms": {"$min": "$elapsed_ms"},
"max_time_ms": {"$max": "$elapsed_ms"},
"count": {"$sum": 1}
}},
{"$sort": {"count": -1}}
]
return list(solves.aggregate(pipeline))
def get_hourly_solve_volume(days=7):
"""Hourly solve volume for charting."""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
pipeline = [
{"$match": {"submitted_at": {"$gte": cutoff}}},
{"$group": {
"_id": {
"date": {"$dateToString": {"format": "%Y-%m-%d", "date": "$submitted_at"}},
"hour": {"$hour": "$submitted_at"}
},
"total": {"$sum": 1},
"solved": {"$sum": {"$cond": [{"$eq": ["$status", "solved"]}, 1, 0]}}
}},
{"$sort": {"_id.date": 1, "_id.hour": 1}}
]
return list(solves.aggregate(pipeline))
def get_error_breakdown(hours=24):
"""Error frequency by error code."""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
pipeline = [
{"$match": {"submitted_at": {"$gte": cutoff}, "status": "error"}},
{"$group": {"_id": "$error", "count": {"$sum": 1}}},
{"$sort": {"count": -1}}
]
return list(solves.aggregate(pipeline))
Implementasi Node.js
Logika yang sama dalam JavaScript untuk tim yang stack utamanya Node.js — koneksi, index, fungsi solve-and-store, dan penghitungan tingkat keberhasilan:
const { MongoClient } = require("mongodb");
const axios = require("axios");
const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017";
const API_KEY = process.env.CAPTCHAAI_API_KEY;
let db, solves;
async function connect() {
const client = await MongoClient.connect(MONGO_URI);
db = client.db("captcha_tracking");
solves = db.collection("solves");
await solves.createIndex({ submitted_at: -1 });
await solves.createIndex({ type: 1, status: 1 });
await solves.createIndex({ "metadata.project": 1 });
await solves.createIndex(
{ submitted_at: 1 },
{ expireAfterSeconds: 90 * 24 * 3600 }
);
}
async function solveAndStore(sitekey, pageurl, type = "recaptcha_v2", metadata = {}) {
const submittedAt = new Date();
const { insertedId } = await solves.insertOne({
type, method: "userrecaptcha", sitekey, pageurl,
status: "submitted", submitted_at: submittedAt, metadata,
});
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) {
await solves.updateOne({ _id: insertedId }, { $set: { status: "error", error: submit.data.request } });
return null;
}
const captchaId = submit.data.request;
await solves.updateOne({ _id: insertedId }, { $set: { captcha_id: captchaId, status: "polling" } });
let polls = 0;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
polls++;
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 solvedAt = new Date();
await solves.updateOne({ _id: insertedId }, { $set: {
status: "solved", solution: poll.data.request,
solved_at: solvedAt, elapsed_ms: solvedAt - submittedAt, polls,
}});
return poll.data.request;
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
await solves.updateOne({ _id: insertedId }, { $set: { status: "error", error: poll.data.request, polls } });
return null;
}
}
await solves.updateOne({ _id: insertedId }, { $set: { status: "timeout", polls } });
return null;
}
async function getSuccessRate(hours = 24) {
const cutoff = new Date(Date.now() - hours * 3600 * 1000);
const pipeline = [
{ $match: { submitted_at: { $gte: cutoff } } },
{ $group: { _id: "$status", count: { $sum: 1 } } },
];
const results = await solves.aggregate(pipeline).toArray();
const total = results.reduce((s, r) => s + r.count, 0);
const solved = results.find((r) => r._id === "solved")?.count || 0;
return total ? ((solved / total) * 100).toFixed(1) : 0;
}
Strategi Retensi Data
Jangan simpan semuanya selamanya secara default — putuskan retensi sesuai kebutuhan sebelum koleksi membengkak:
| Strategi | Index TTL | Kasus Pakai |
|---|---|---|
| Retensi 30 hari | expireAfterSeconds: 2592000 |
Development/testing |
| Retensi 90 hari | expireAfterSeconds: 7776000 |
Analitik produksi |
| Permanen (dengan arsip) | Tanpa TTL; pakai capped collection atau cold storage | Kepatuhan/audit |
Untuk tim yang beroperasi di Indonesia, satu catatan kepatuhan yang relevan: UU Pelindungan Data Pribadi (UU 27/2022) dan UU ITE mengharuskan Anda hanya memproses data yang benar-benar berwenang Anda proses. Skema di atas mencatat metadata operasional (jenis CAPTCHA, waktu, status, domain target) — bukan data pribadi pengguna dari halaman yang di-scrape — tapi tetap tinjau field metadata dan pageurl Anda sendiri kalau ada kemungkinan berisi data pribadi pihak lain. Artikel ini bukan nasihat hukum.
Troubleshooting Umum
| Masalah | Penyebab | Solusi |
|---|---|---|
| Kueri aggregation lambat | Index pada submitted_at dan type belum dibuat |
Jalankan setup_indexes() — lihat bagian index di atas |
| Ukuran dokumen membengkak | Solusi lengkap disimpan di setiap catatan | Simpan hash solusi, atau potong setelah dipakai |
| TTL tidak kunjung menghapus catatan lama | Monitor TTL MongoDB jalan tiap 60 detik; backlog besar butuh waktu | Tunggu proses background; cek index lewat db.solves.getIndexes() |
| Connection pool habis | Terlalu banyak operasi solve berjalan bersamaan | Set maxPoolSize di connection string |
Pertanyaan Umum
Apakah menyimpan riwayat solve CAPTCHA berisiko soal UU PDP?
Selama yang disimpan adalah metadata operasional (jenis, waktu, status, error) bukan data pribadi pengguna dari halaman target, risikonya rendah — tapi tetap audit field metadata dan pageurl di skema Anda sendiri, karena ini bukan nasihat hukum.
Index mana yang paling penting kalau volume solve-nya besar?
Index gabungan type + status dan index submitted_at menopang hampir semua kueri di panduan ini. Tanpa keduanya, kueri get_avg_solve_time_by_type() dan get_hourly_solve_volume() akan full collection scan begitu koleksi tumbuh ke jutaan dokumen.
Bisa pakai MongoDB Atlas untuk deployment di Asia Tenggara?
Bisa. Atlas mendukung index TTL dan aggregation pipeline penuh. Pilih region Atlas yang dekat dengan worker CaptchaAI Anda — misalnya region Singapura — supaya latensi koneksi tidak menambah waktu penyelesaian yang tercatat.
Berapa lama sebaiknya token solusi CAPTCHA disimpan?
Untuk debugging, 24–48 jam sudah cukup lalu biarkan index TTL yang membersihkan. Untuk analitik jangka panjang, simpan metadata saja (jenis, waktu, status, error) — token sudah tidak berguna setelah masa berlakunya habis.
Bagaimana cara memantau tingkat keberhasilan secara real-time?
Panggil get_success_rate() dengan window pendek (misalnya 1 jam) dari cron job atau endpoint monitoring, lalu bandingkan dengan window 24 jam. Selisih besar antar keduanya biasanya menandakan ada jenis CAPTCHA tertentu yang baru saja mulai bermasalah.
Langkah Selanjutnya
Lacak setiap solve CAPTCHA dan temukan masalah sebelum berdampak ke pipeline Anda — dapatkan API key CaptchaAI Anda.
Panduan terkait:
- SQLite untuk caching CAPTCHA lokal
- Manajemen TTL token Redis
- Tren kinerja solve CAPTCHA dari waktu ke waktu