Tutorials

Server-Sent Events untuk Notifikasi Solve CAPTCHA Real-Time

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:

  1. Client membuka koneksi ke endpoint SSE Anda (koneksi HTTP persisten).
  2. Client mengirim task CAPTCHA ke CaptchaAI dengan pingback menunjuk ke server Anda.
  3. CaptchaAI menyelesaikan tantangan CAPTCHA lalu memanggil endpoint callback Anda.
  4. 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: no terpasang 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:

Komentar dinonaktifkan untuk artikel ini.