Jawaban singkatnya: pasang satu endpoint SSE di server Anda, arahkan parameter pingback CaptchaAI ke server yang sama, lalu dorong hasilnya ke browser begitu callback masuk. Dashboard Anda menerima token dalam hitungan sepersekian detik, dan tidak ada satu pun request polling yang terbuang.
Perbedaannya paling terasa pada dashboard internal. Dengan polling res.php tiap 5 detik, sepuluh operator yang membuka halaman monitoring menembakkan ratusan request per menit yang sebagian besar dijawab "belum siap". Dengan SSE, koneksi HTTP dibuka sekali dan tetap terbuka: server yang berbicara, client mendengarkan.
Kapan SSE lebih tepat daripada polling atau WebSocket
Pilih SSE ketika arah datanya satu arah — server ke browser. Hasil penyelesaian CAPTCHA persis seperti itu: client mengirim task, lalu diam sampai token datang.
| Fitur | SSE | WebSocket | Polling |
|---|---|---|---|
| Arah data | Server ke client | Dua arah | Client ke server |
| Protokol | HTTP/1.1+ | WS/WSS | HTTP |
| Reconnect otomatis | Bawaan | Manual | Tidak relevan |
| Dukungan browser | Semua browser modern | Semua browser modern | Semua |
| Kompleksitas | Rendah | Sedang | Rendah |
| Request terbuang | Tidak ada | Tidak ada | Banyak |
| Cocok untuk hasil CAPTCHA | Ya | Berlebihan | Jalan, tapi boros |
WebSocket baru masuk akal kalau browser juga perlu mengirim perintah balik terus-menerus. Untuk sekadar mengantar token yang sudah selesai, SSE lebih ringan, lolos proxy HTTP biasa, dan reconnect-nya sudah ditangani EventSource tanpa kode tambahan.
Ringkasnya, tiga aturan praktis:
- Dashboard atau halaman web yang cuma menampilkan hasil — pakai SSE.
- Ada interaksi dua arah, misalnya operator membatalkan task dari layar — baru pakai WebSocket.
- Worker backend tanpa browser sama sekali — tangani callback langsung, tidak perlu keduanya.
Alur kerja: dari task masuk sampai token tampil di layar
[Client] ← SSE stream ← [Your Server] ← Callback ← [CaptchaAI]
↓ ↑
Submit task → [CaptchaAI] ──┘ (pingback URL points to your server)
Empat langkah, dan urutannya tidak berubah:
- Client membuka koneksi ke endpoint SSE Anda (koneksi HTTP persisten).
- Client mengirim task CAPTCHA ke CaptchaAI dengan
pingbackmenunjuk ke server Anda. - CaptchaAI menyelesaikan tantangan CAPTCHA lalu memanggil endpoint callback Anda.
- Server Anda meneruskan hasilnya lewat stream SSE ke client yang tepat.
Server Anda adalah perantaranya. CaptchaAI tidak pernah berbicara langsung ke browser, jadi endpoint callback wajib terjangkau dari internet publik — ini titik gagal nomor satu saat orang mencobanya dari localhost.
Implementasi Python dengan Flask
Sisi server
Satu proses Flask menangani tiga hal sekaligus: menyimpan antrean per client, menerima task, dan menerima callback.
import os
import queue
import threading
import requests
from flask import Flask, Response, request, jsonify
app = Flask(__name__)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Per-client event queues: client_id -> Queue
client_queues = {}
queues_lock = threading.Lock()
@app.route("/events/<client_id>")
def sse_stream(client_id):
"""SSE endpoint — clients connect here for real-time results."""
q = queue.Queue()
with queues_lock:
client_queues[client_id] = q
def generate():
try:
while True:
# Block until a result arrives (timeout for keepalive)
try:
data = q.get(timeout=30)
yield f"event: captcha-solved\ndata: {data}\n\n"
except queue.Empty:
# Send keepalive comment to prevent connection timeout
yield ": keepalive\n\n"
finally:
with queues_lock:
client_queues.pop(client_id, None)
return Response(
generate(),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no" # Disable nginx buffering
}
)
@app.route("/submit", methods=["POST"])
def submit_captcha():
"""Submit a CAPTCHA task with callback to this server."""
data = request.json
client_id = data["client_id"]
sitekey = data["sitekey"]
pageurl = data["pageurl"]
callback_url = f"{request.host_url}callback?client_id={client_id}"
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"pingback": callback_url,
"json": 1
})
result = resp.json()
if result.get("status") == 1:
return jsonify({"task_id": result["request"]})
return jsonify({"error": result.get("request")}), 400
@app.route("/callback")
def captcha_callback():
"""Receive CaptchaAI callback and push to SSE stream."""
client_id = request.args.get("client_id")
task_id = request.args.get("id")
solution = request.args.get("code")
import json
message = json.dumps({
"task_id": task_id,
"solution": solution
})
with queues_lock:
q = client_queues.get(client_id)
if q:
q.put(message)
return "OK", 200
if __name__ == "__main__":
app.run(port=5000, threaded=True)
Perhatikan komentar keepalive. Tanpa itu, proxy atau load balancer memutus koneksi yang diam lebih dari 30 detik, dan browser reconnect berulang-ulang tanpa alasan jelas di log Anda.
Sisi browser
<!DOCTYPE html>
<html>
<body>
<button onclick="submitCaptcha()">Solve CAPTCHA</button>
<div id="results"></div>
<script>
const clientId = crypto.randomUUID();
const resultsDiv = document.getElementById("results");
// Connect SSE stream
const eventSource = new EventSource(`/events/${clientId}`);
eventSource.addEventListener("captcha-solved", (event) => {
const data = JSON.parse(event.data);
resultsDiv.innerHTML += `<p>Task ${data.task_id}: ${data.solution.substring(0, 30)}...</p>`;
});
eventSource.onerror = () => {
console.log("SSE connection lost, reconnecting...");
};
async function submitCaptcha() {
const response = await fetch("/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: clientId,
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com"
})
});
const result = await response.json();
resultsDiv.innerHTML += `<p>Submitted: ${result.task_id}</p>`;
}
</script>
</body>
</html>
EventSource menangani reconnect sendiri. Anda cukup mendengarkan nama event yang dikirim server — di sini captcha-solved.
Implementasi JavaScript dengan Express
Kalau stack Anda Node.js, polanya sama persis: Map menggantikan dictionary antrean, dan objek Response disimpan langsung karena Express mengizinkan penulisan ke response yang masih terbuka.
const express = require("express");
const axios = require("axios");
const app = express();
app.use(express.json());
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
// Per-client SSE connections: clientId -> Response object
const clients = new Map();
// SSE endpoint
app.get("/events/:clientId", (req, res) => {
const clientId = req.params.clientId;
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
});
clients.set(clientId, res);
// Keepalive every 30 seconds
const keepalive = setInterval(() => {
res.write(": keepalive\n\n");
}, 30000);
req.on("close", () => {
clearInterval(keepalive);
clients.delete(clientId);
});
});
// Submit CAPTCHA
app.post("/submit", async (req, res) => {
const { client_id, sitekey, pageurl } = req.body;
const callbackUrl = `${BASE_URL}/callback?client_id=${client_id}`;
try {
const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
pingback: callbackUrl,
json: 1,
},
});
if (resp.data.status === 1) {
return res.json({ task_id: resp.data.request });
}
res.status(400).json({ error: resp.data.request });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// CaptchaAI callback → push to SSE
app.get("/callback", (req, res) => {
const clientId = req.query.client_id;
const taskId = req.query.id;
const solution = req.query.code;
const clientRes = clients.get(clientId);
if (clientRes) {
const data = JSON.stringify({ task_id: taskId, solution: solution });
clientRes.write(`event: captcha-solved\ndata: ${data}\n\n`);
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("SSE server running on :3000"));
Catatan deployment untuk tim di Indonesia
Latensi berperan lebih besar dari yang orang kira. Kalau server aplikasi Anda di AWS ap-southeast-3 (Jakarta) atau GCP asia-southeast2 sementara worker automation jalan di ap-southeast-1 (Singapura), setiap siklus polling menambah satu round-trip lintas region. SSE memindahkan biaya itu ke satu koneksi saja.
Konteks kedua: banyak tim automation dan agensi price-monitoring di sini beranggaran ketat, dan operatornya memantau dari perangkat mobile. Polling agresif lewat koneksi seluler yang naik-turun menghasilkan request gagal dan data terbuang; satu stream SSE dengan keepalive 30 detik jauh lebih bersahabat.
Paket CaptchaAI dihitung per thread bersamaan, bukan per solve, jadi pindah ke SSE tidak mengubah tagihan Anda — yang berkurang adalah beban server aplikasi sendiri. Ukuran paket mengikuti jumlah task bersamaan, bukan jumlah operator yang menonton dashboard:
- BASIC ($15/bulan, 5 thread) — satu worker automation, dashboard internal kecil.
- STANDARD ($30/bulan, 15 thread) — beberapa job scraping paralel.
- ADVANCE ($90/bulan, 50 thread) — antrean task yang padat sepanjang hari.
Harga selalu dalam USD.
Menyiapkan SSE untuk produksi
Beberapa instance di belakang load balancer
Koneksi SSE bersifat stateful. Kalau aplikasi Anda berjalan di tiga instance, callback dari CaptchaAI bisa mendarat di instance yang berbeda dari instance yang memegang koneksi browser. Hasilnya: token diterima server, tapi tidak pernah sampai ke layar.
Solusinya adalah bus pesan. Redis Pub/Sub sudah cukup untuk sebagian besar kasus:
# Callback handler publishes to Redis
import redis
r = redis.Redis()
r.publish(f"captcha:{client_id}", json.dumps(message))
# SSE handler subscribes to Redis
pubsub = r.pubsub()
pubsub.subscribe(f"captcha:{client_id}")
for msg in pubsub.listen():
if msg["type"] == "message":
yield f"data: {msg['data'].decode()}\n\n"
Batas koneksi browser
Di HTTP/1.1, browser membatasi 6 koneksi per domain dan satu tab dengan beberapa stream SSE cepat menghabiskannya. Aktifkan HTTP/2, atau lebih baik: multiplex semua hasil task lewat satu koneksi per client dan bedakan dengan task_id.
Checklist sebelum go-live
- Endpoint callback dapat dijangkau publik dan memakai HTTPS.
- Header
X-Accel-Buffering: noterpasang kalau Anda berada di belakang nginx. - Batas waktu proxy lebih besar daripada interval keepalive.
- Callback divalidasi sebelum dipercaya — lihat panduan keamanan webhook.
- Tersedia fallback polling untuk client yang koneksinya sering putus.
Diagnosis masalah yang paling sering muncul
| Masalah | Penyebab | Perbaikan |
|---|---|---|
| Koneksi putus tiap 30 detik | Batas waktu proxy atau load balancer | Kirim komentar keepalive; perbesar batas waktu proxy |
| Hasil tidak pernah sampai | Callback mendarat di instance server lain | Tambahkan Redis Pub/Sub antara handler callback dan handler SSE |
| Browser menampilkan error di console | Header CORS belum ada | Tambahkan Access-Control-Allow-Origin di endpoint SSE |
| Reconnect berulang-ulang | Format event SSE salah | Pastikan setiap event diakhiri dua baris baru; validasi format data |
| Callback tidak pernah dipanggil | pingback menunjuk ke alamat lokal |
Gunakan domain publik, bukan localhost atau IP privat |
Pertanyaan umum
Apakah pingback CaptchaAI tersedia untuk semua tipe yang didukung?
Ya. Parameter pingback bekerja pada tipe yang memang didukung: reCAPTCHA v2 dan v3 beserta variannya, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, serta image dan grid. hCaptcha dan FunCaptcha belum didukung, dan GeeTest v4 masih berstatus segera hadir.
Berapa lama koneksi SSE boleh dibiarkan terbuka?
Selama proxy dan load balancer Anda mengizinkannya. Praktik yang aman: kirim komentar keepalive tiap 30 detik dan set batas waktu proxy di atas angka itu, misalnya 120 detik. Browser akan reconnect otomatis kalau koneksi tetap terputus.
Bisakah SSE dipakai berdampingan dengan polling res.php?
Bisa, dan untuk sistem kritis justru disarankan. Jadikan SSE jalur utama, lalu jalankan polling berjeda longgar — misalnya 30 detik — sebagai jaring pengaman kalau ada callback yang hilang.
Apa yang terjadi kalau browser sedang offline saat callback masuk?
Pesan itu hilang; SSE tidak menyimpan riwayat. Simpan hasil di Redis atau database dengan masa berlaku pendek, lalu kirim ulang saat client reconnect dengan Last-Event-ID.
Apakah pendekatan ini cocok untuk worker CLI, bukan browser?
Untuk worker backend atau tool CLI, menangani callback langsung atau memakai antrean seperti Redis dan Kafka lebih sederhana. SSE paling menguntungkan ketika targetnya dashboard browser atau aplikasi web.
Langkah berikutnya
Mulai dari yang terkecil: satu client, satu task, satu stream. Setelah token pertama muncul di layar tanpa polling, barulah tambahkan Redis Pub/Sub dan multi-instance. Ambil API key CaptchaAI Anda dan sambungkan SSE ke jalur callback yang sudah Anda punya.
Panduan terkait: